Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766b6e81d0 | ||
|
|
0349e01629 | ||
|
|
0bd9da2bbc | ||
|
|
f14c7c3f55 | ||
|
|
ef5793fb15 | ||
|
|
9f22fe6d97 | ||
|
|
3147d576ee | ||
|
|
2ceec2588c | ||
|
|
5b4d88b9c7 | ||
|
|
55043cca36 | ||
|
|
097ebe025f | ||
|
|
9f92307f64 | ||
|
|
c220035fd9 | ||
|
|
d7b98d3cde | ||
|
|
0ab876633f | ||
|
|
cabe76ca54 | ||
|
|
c57224e258 | ||
|
|
b56101d2e6 | ||
|
|
16211c28d1 | ||
|
|
139d3e0c3d | ||
|
|
f0652fc5ec | ||
|
|
55489cd634 | ||
|
|
742887c0e4 | ||
|
|
c3e5cd2c0d | ||
|
|
63e125627c | ||
|
|
c0189e9d05 | ||
|
|
133e3208ec | ||
|
|
17c6fd6d0f | ||
|
|
2aa7ea43b8 | ||
|
|
6d7dbfac31 | ||
|
|
d4942c6dd6 | ||
|
|
39ae3c24f6 | ||
|
|
a04d74a949 | ||
|
|
b93a7ebd94 | ||
|
|
cbc7b0340f | ||
|
|
ea7a197848 | ||
|
|
82d4df889e | ||
|
|
df3a2a1010 | ||
|
|
67cd6c2f59 | ||
|
|
b1788e7df9 | ||
|
|
1cb8a81518 | ||
|
|
e1b61f2675 | ||
|
|
834d44856c | ||
|
|
c088eeb5bc | ||
|
|
921510b7b7 | ||
|
|
17a54f7a8a | ||
|
|
43ff924efc | ||
|
|
f9bd4b6628 | ||
|
|
702aa22139 | ||
|
|
57d53f63e5 | ||
|
|
835ada4a53 | ||
|
|
7f204f3e1b | ||
|
|
70e13b8414 | ||
|
|
df9bb8cbb8 | ||
|
|
cebdb32502 | ||
|
|
58bd61343a | ||
|
|
90faf1007e | ||
|
|
f8dbd12b69 | ||
|
|
cf6ba66dd2 | ||
|
|
2080b5d10a | ||
|
|
dd180977d9 | ||
|
|
221dcbaf1b | ||
|
|
1a2098f4fb | ||
|
|
73eab17110 | ||
|
|
978a336358 | ||
|
|
6a0fd4e427 | ||
|
|
5fd4efd920 | ||
|
|
43be2987f5 | ||
|
|
06cc048fdc | ||
|
|
5752471420 | ||
|
|
3dfe12b90a | ||
|
|
ad1e3501b1 | ||
|
|
1b46572e4c | ||
|
|
9d8e42d34c | ||
|
|
3f6e9dab3e | ||
|
|
ad85f11875 | ||
|
|
9ae31834cc | ||
|
|
49a4dc6942 | ||
|
|
a9f58cc30f | ||
|
|
bdb2198582 | ||
|
|
f32a77ca8b | ||
|
|
121d3fb0ae | ||
|
|
1dd72f7600 | ||
|
|
c7c168b007 | ||
|
|
441b41c6af | ||
|
|
49a907283a | ||
|
|
0ffc52f9ba | ||
|
|
4e96ec49e7 | ||
|
|
dfb4646f51 | ||
|
|
a8e82c38c9 | ||
|
|
dba23289fd | ||
|
|
c2c1eb98d9 | ||
|
|
364176912d | ||
|
|
202245baad | ||
|
|
c3fa99640c | ||
|
|
a7c34b5a6a | ||
|
|
05de092642 | ||
|
|
1bfa40c841 | ||
|
|
c325435ae8 | ||
|
|
b978bd41c3 | ||
|
|
f03d8733b0 | ||
|
|
a624b3b759 | ||
|
|
d683398ba9 | ||
|
|
42f692b82d | ||
|
|
855fe9115e | ||
|
|
d6bd383822 | ||
|
|
b1b77b73ac | ||
|
|
8d87adcc50 | ||
|
|
49bf6c54e7 | ||
|
|
7e92e0b1d9 | ||
|
|
f0805d320c | ||
|
|
364f48a6f0 | ||
|
|
cfebf6df0e | ||
|
|
688855b72d | ||
|
|
77b275a04b | ||
|
|
98b142df12 | ||
|
|
c5fe845f18 | ||
|
|
e5e9629d31 | ||
|
|
b65b9e6bfb | ||
|
|
93ae8b02e3 | ||
|
|
5c183ea200 | ||
|
|
661765f08c | ||
|
|
60fa19c681 | ||
|
|
606eddba53 | ||
|
|
71605ade31 | ||
|
|
ee409e4872 | ||
|
|
91024abc4a | ||
|
|
99de36a6cf | ||
|
|
ac1e48dc30 | ||
|
|
494bab3380 | ||
|
|
6696ca93ce | ||
|
|
b7e80fb8ed | ||
|
|
ce5b642d3e | ||
|
|
76d414164a | ||
|
|
b6d13047ba | ||
|
|
f1ba295796 | ||
|
|
415abef4fc | ||
|
|
95af2014d8 | ||
|
|
f75b039daf | ||
|
|
65b1ac78f3 | ||
|
|
b067591882 | ||
|
|
7941d76fd7 | ||
|
|
7bff601beb | ||
|
|
7b4da32bee | ||
|
|
bbf2974703 | ||
|
|
f5abaf06de | ||
|
|
88023dd09e | ||
|
|
eec2996fab | ||
|
|
97ca200c29 | ||
|
|
f0600ea3a3 | ||
|
|
19b3eb51d7 | ||
|
|
871dabf142 | ||
|
|
58795b9349 | ||
|
|
1ef769842b | ||
|
|
6051570414 | ||
|
|
da5d5d0dc7 | ||
|
|
782aa22d4d | ||
|
|
0084867f38 | ||
|
|
5a27858f86 | ||
|
|
7905d8375f | ||
|
|
ba8aa63d75 | ||
|
|
ac82778e26 | ||
|
|
55d2661e38 | ||
|
|
4803b89294 | ||
|
|
e48adbd87d | ||
|
|
227314d83c | ||
|
|
3c0e21eb02 | ||
|
|
86d6153ef1 | ||
|
|
78cf8bded3 | ||
|
|
26d12181cd | ||
|
|
8d184b011c | ||
|
|
ed3a1444ec | ||
|
|
d3e70a47cd | ||
|
|
3171151219 | ||
|
|
e407867635 | ||
|
|
6d9cfab55f | ||
|
|
f0bce029f0 | ||
|
|
d47960a0cf | ||
|
|
8416018afe | ||
|
|
e93fc70169 | ||
|
|
5b3474c781 | ||
|
|
8d2a9f3b2b | ||
|
|
73d09f9b09 | ||
|
|
4e7c6efa2a | ||
|
|
81a884bc61 | ||
|
|
ac4f960352 | ||
|
|
e4b3e4e438 | ||
|
|
d6ee476296 | ||
|
|
c23741ba9a | ||
|
|
7bac5b4740 | ||
|
|
1fe440168e | ||
|
|
26ad57138b | ||
|
|
92e1fdaf0f | ||
|
|
357540d0ad | ||
|
|
405622a91e | ||
|
|
5686c73c56 | ||
|
|
76e2f787ac | ||
|
|
02b67f4e6a | ||
|
|
a2ab527761 | ||
|
|
f1142ae742 | ||
|
|
decb222dac | ||
|
|
00c598e6f7 | ||
|
|
e9843dd3a5 | ||
|
|
37f390742b | ||
|
|
533e7dfd15 | ||
|
|
ddcbe1cd51 | ||
|
|
4eabd04bc6 | ||
|
|
c8f2da6ac9 | ||
|
|
8820ac4bf3 | ||
|
|
236b200f39 | ||
|
|
6789e32674 | ||
|
|
9eb55deedf | ||
|
|
6e76b5b8eb | ||
|
|
491b0da83d | ||
|
|
5d2c17e940 | ||
|
|
0497c4a9e7 | ||
|
|
6d0765848b | ||
|
|
40c4ae71bd | ||
|
|
8edba10dff | ||
|
|
85aa7bc652 | ||
|
|
854ad1db07 | ||
|
|
955600a1e1 | ||
|
|
b739134d85 | ||
|
|
723152566d | ||
|
|
678b045e0e | ||
|
|
681f4df49a | ||
|
|
b19f7d7098 | ||
|
|
daaae3ec10 |
No files matched your search
File renamed without changes.
@@ -1,55 +0,0 @@
|
||||
from PIL import Image
|
||||
import numpy, pefile, os
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def preprocess_image(file_data, size, channels=1):
|
||||
width, height = size
|
||||
wah = int(numpy.ceil(numpy.sqrt(len(file_data) / channels)))
|
||||
arr = numpy.frombuffer(file_data, dtype=numpy.uint8)
|
||||
img = numpy.zeros(wah*wah*channels, dtype=numpy.uint8)
|
||||
img[:len(file_data)] = arr
|
||||
image = Image.fromarray(img.reshape((wah, wah)), 'L')
|
||||
return image.resize((width, height), Image.Resampling.NEAREST)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def get_type(file_path):
|
||||
suffix = {".com", ".dll", ".drv", ".exe", ".ocx", ".scr", ".sys", ".mui", ".cpl"}
|
||||
try:
|
||||
file_content = None
|
||||
if not os.path.splitext(file_path)[-1].lower() in suffix:
|
||||
return file_content
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
except:
|
||||
pass
|
||||
return file_content
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def file_to_images(input_dir, output_dir):
|
||||
for root, dirs, files in os.walk(input_dir):
|
||||
for file in files:
|
||||
try:
|
||||
file_path = os.path.join(root, file)
|
||||
file_content = get_type(file_path)
|
||||
if file_content:
|
||||
print(file_path)
|
||||
relative_path = os.path.relpath(root, input_dir)
|
||||
output_path_dir = os.path.join(output_dir, relative_path)
|
||||
|
||||
if not os.path.exists(output_path_dir):
|
||||
os.makedirs(output_path_dir)
|
||||
image = preprocess_image(file_content, (224, 224))
|
||||
safe_name = os.path.basename(file_path).replace('/', '_').replace('\\', '_')
|
||||
image.save(os.path.join(output_path_dir, f"{safe_name}.png"))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
input_directory = input('Enter the folder path: ')
|
||||
output_directory = "./Image_Output"
|
||||
file_to_images(input_directory, output_directory)
|
||||
input('Conversion Complete')
|
||||
@@ -1,34 +0,0 @@
|
||||
import os, hashlib
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def calc_sha256(path):
|
||||
with open(path, "rb") as f:
|
||||
return hashlib.sha256(f.read()).digest()
|
||||
|
||||
def dedup_realtime(root):
|
||||
seen = set()
|
||||
deleted = 0
|
||||
for dirpath, _, files in os.walk(root):
|
||||
for name in files:
|
||||
p = os.path.join(dirpath, name)
|
||||
try:
|
||||
h = calc_sha256(p)
|
||||
except Exception:
|
||||
continue
|
||||
if h in seen:
|
||||
try:
|
||||
os.remove(p)
|
||||
print(p)
|
||||
deleted += 1
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
seen.add(h)
|
||||
print(f"deleted {deleted}")
|
||||
|
||||
####################################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
dedup_realtime(input("Input file path: "))
|
||||
input('Deduplicate Complete')
|
||||
@@ -1,163 +0,0 @@
|
||||
import tensorflow as tf
|
||||
import os, tf2onnx, numpy, random
|
||||
from PIL import Image
|
||||
from tensorflow.keras import *
|
||||
from collections import Counter
|
||||
|
||||
####################################################################################################
|
||||
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
tf.keras.mixed_precision.set_global_policy('mixed_float16')
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def load_dataset(data_dir, image_size=(224, 224), val_split=0.00001, batch_size=256, color_mode="grayscale"):
|
||||
channels = 1 if color_mode == "grayscale" else 3
|
||||
file_paths, labels, class_indices = get_file_list(data_dir)
|
||||
|
||||
combined = list(zip(file_paths, labels))
|
||||
random.shuffle(combined)
|
||||
file_paths[:], labels[:] = zip(*combined)
|
||||
|
||||
num_val = int(val_split * len(file_paths))
|
||||
train_files = file_paths[num_val:]
|
||||
train_labels = labels[num_val:]
|
||||
val_files = file_paths[:num_val]
|
||||
val_labels = labels[:num_val]
|
||||
num_classes = len(class_indices)
|
||||
|
||||
train_ds = tf.data.Dataset.from_tensor_slices((train_files, train_labels))
|
||||
train_ds = train_ds.map(lambda f, l: parse_fn(f, l, image_size, channels, num_classes), num_parallel_calls=tf.data.AUTOTUNE)
|
||||
train_ds = train_ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)
|
||||
|
||||
val_ds = tf.data.Dataset.from_tensor_slices((val_files, val_labels))
|
||||
val_ds = val_ds.map(lambda f, l: parse_fn(f, l, image_size, channels, num_classes), num_parallel_calls=tf.data.AUTOTUNE)
|
||||
val_ds = val_ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)
|
||||
|
||||
train_ds.class_indices = class_indices
|
||||
return train_ds, val_ds
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def parse_fn(filename, label, image_size, channels, num_classes):
|
||||
img = tf.io.read_file(filename)
|
||||
img = tf.image.decode_image(img, channels=channels, expand_animations=False)
|
||||
img = tf.cond(tf.reduce_all(tf.equal(tf.shape(img)[:2], image_size)), lambda: tf.cast(img, tf.float32), lambda: tf.image.resize(img, image_size))
|
||||
img = tf.cast(img, tf.float32) / 255.0
|
||||
label = tf.one_hot(label, depth=num_classes)
|
||||
return img, label
|
||||
|
||||
def get_file_list(directory):
|
||||
file_paths, labels, class_indices = [], [], {}
|
||||
for i, cls in enumerate(sorted([d for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))])):
|
||||
class_indices[cls] = i
|
||||
cls_dir = os.path.join(directory, cls)
|
||||
|
||||
for root, _, files in os.walk(cls_dir):
|
||||
for file in files:
|
||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
file_paths.append(os.path.join(root, file))
|
||||
labels.append(i)
|
||||
return file_paths, labels, class_indices
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def multi_conv(x, strides):
|
||||
convs = []
|
||||
for k in [(1, 7), (1, 1), (3, 3), (5, 5)]:
|
||||
c = layers.DepthwiseConv2D(kernel_size=k, strides=strides, padding='same', activation='gelu')(x)
|
||||
convs.append(c)
|
||||
|
||||
x = layers.Concatenate()(convs)
|
||||
x = layers.BatchNormalization()(x)
|
||||
return layers.Activation('gelu')(x)
|
||||
|
||||
def gated_conv(x, strides):
|
||||
c = layers.DepthwiseConv2D(3, strides=strides, padding='same', activation='gelu')(x)
|
||||
g = layers.DepthwiseConv2D(3, strides=strides, padding='same', activation='sigmoid')(x)
|
||||
return layers.Multiply()([c, g])
|
||||
|
||||
def coordinate_attention(x, reduction):
|
||||
input_shape = tf.shape(x)
|
||||
h, w = input_shape[1], input_shape[2]
|
||||
filters = x.shape[-1]
|
||||
mip = max(8, filters // reduction)
|
||||
|
||||
x_h = layers.Lambda(lambda t: tf.reduce_mean(t, axis=2, keepdims=True))(x)
|
||||
x_w = layers.Lambda(lambda t: tf.reduce_mean(t, axis=1, keepdims=True))(x)
|
||||
x_w_t = layers.Permute((2, 1, 3))(x_w)
|
||||
|
||||
x_cat = layers.Concatenate(axis=1)([x_h, x_w_t])
|
||||
x_cat = layers.Conv2D(mip, 1, padding='valid', use_bias=False)(x_cat)
|
||||
x_cat = layers.BatchNormalization()(x_cat)
|
||||
x_cat = layers.Activation('gelu')(x_cat)
|
||||
|
||||
x_cat_h, x_cat_w = layers.Lambda(lambda t: tf.split(t[0], [t[1], t[2]], axis=1))([x_cat, h, w])
|
||||
x_h_sigmoid = layers.Conv2D(filters, 1, padding='valid', activation='sigmoid')(x_cat_h)
|
||||
x_cat_w = layers.Permute((2, 1, 3))(x_cat_w)
|
||||
x_w_sigmoid = layers.Conv2D(filters, 1, padding='valid', activation='sigmoid')(x_cat_w)
|
||||
|
||||
return layers.Multiply()([x, x_h_sigmoid, x_w_sigmoid])
|
||||
|
||||
def build_model(input_shape, num_classes):
|
||||
i = layers.Input(shape=input_shape)
|
||||
x = multi_conv(i, strides=1)
|
||||
x = gated_conv(x, strides=2)
|
||||
x = multi_conv(x, strides=2)
|
||||
x = multi_conv(x, strides=2)
|
||||
x = multi_conv(x, strides=2)
|
||||
x = multi_conv(x, strides=2)
|
||||
|
||||
x = coordinate_attention(x, reduction=4)
|
||||
x = layers.GlobalMaxPooling2D()(x)
|
||||
|
||||
x = layers.Dense(x.shape[-1] // 2, activation='gelu', dtype='float32')(x)
|
||||
o = layers.Dense(num_classes, activation='softmax', dtype='float32')(x)
|
||||
return models.Model(i, o)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def calculate_lr_cosine_sq(epoch, total_epochs=30, lr_start=5e-4, lr_end=5e-7):
|
||||
cos_val = 0.5 * (1 + numpy.cos(epoch / total_epochs * numpy.pi))
|
||||
return lr_end + (lr_start - lr_end) * (cos_val ** 2)
|
||||
|
||||
def lr_scheduler(epoch):
|
||||
return calculate_lr_cosine_sq(epoch)
|
||||
|
||||
def categorical_focal_loss(gamma=2.0, alpha=0.25):
|
||||
def focal_loss(y_true, y_pred):
|
||||
y_pred = tf.clip_by_value(y_pred, tf.keras.backend.epsilon(), 1.0 - tf.keras.backend.epsilon())
|
||||
cross_entropy = -y_true * tf.math.log(y_pred)
|
||||
loss = alpha * tf.math.pow(1.0 - y_pred, gamma) * cross_entropy
|
||||
return tf.reduce_sum(loss, axis=-1)
|
||||
return focal_loss
|
||||
|
||||
class CustomModelCheckpoint(callbacks.Callback):
|
||||
def on_epoch_end(self, epoch, logs=False):
|
||||
models.save_model(self.model, f"PYAS_Model_Epoch_{epoch + 1}.h5")
|
||||
spec = (tf.TensorSpec((None,) + train_ds.image_shape, tf.float32),)
|
||||
model_proto, _ = tf2onnx.convert.from_keras(self.model, input_signature=spec, opset=17)
|
||||
with open(f"PYAS_Model_Epoch_{epoch + 1}.onnx", "wb") as f:
|
||||
f.write(model_proto.SerializeToString())
|
||||
|
||||
####################################################################################################
|
||||
|
||||
train_ds, val_ds = load_dataset(r'.\Image_Full_Pefile')
|
||||
for imgs, _ in train_ds.take(1):
|
||||
train_ds.image_shape = imgs.shape[1:]
|
||||
break
|
||||
|
||||
try:
|
||||
model = models.load_model('PYAS_Model_Epoch_0.h5')
|
||||
print("Load model from disk")
|
||||
except:
|
||||
print("Creating a new model")
|
||||
model = build_model(train_ds.image_shape, len(train_ds.class_indices))
|
||||
|
||||
print(f"Total parameters: {model.count_params()}")
|
||||
model.compile(optimizer='adam', loss=categorical_focal_loss(gamma=2.0, alpha=0.25),
|
||||
metrics=['accuracy', metrics.Precision(name='precision'), metrics.Recall(name='recall'),
|
||||
metrics.FalsePositives(name='fp'), metrics.TrueNegatives(name='tn')])
|
||||
|
||||
model.fit(train_ds, epochs=30, callbacks=[CustomModelCheckpoint(), callbacks.LearningRateScheduler(lr_scheduler)], validation_data=val_ds)
|
||||
input('Training Complete')
|
||||
@@ -0,0 +1,705 @@
|
||||
import re, os, sys, math, time, zlib, datetime, hashlib
|
||||
import ctypes, ctypes.wintypes, pefile, multiprocessing
|
||||
import numpy as np
|
||||
|
||||
####################################################################################################
|
||||
|
||||
try:
|
||||
import orjson
|
||||
JSON_DUMPS = lambda x: orjson.dumps(x).decode('utf-8')
|
||||
JSON_LOADS = orjson.loads
|
||||
HAS_ORJSON = True
|
||||
except ImportError:
|
||||
import json
|
||||
JSON_DUMPS = json.dumps
|
||||
JSON_LOADS = json.loads
|
||||
HAS_ORJSON = False
|
||||
|
||||
JSONL_PATH = "pe_features.jsonl"
|
||||
BATCH_SIZE = 1000
|
||||
MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024
|
||||
MAX_WORKERS = os.cpu_count() - 2
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class WINTRUST_FILE_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbStruct", ctypes.wintypes.DWORD),
|
||||
("pcwszFilePath", ctypes.wintypes.LPCWSTR),
|
||||
("hFile", ctypes.wintypes.HANDLE),
|
||||
("pgKnownSubject", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA_UNION(ctypes.Union):
|
||||
_fields_ = [
|
||||
("pFile", ctypes.POINTER(WINTRUST_FILE_INFO)),
|
||||
("pCatalog", ctypes.wintypes.LPVOID),
|
||||
("pBlob", ctypes.wintypes.LPVOID),
|
||||
("pSgnr", ctypes.wintypes.LPVOID),
|
||||
("pCert", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbStruct", ctypes.wintypes.DWORD),
|
||||
("pPolicyCallbackData", ctypes.wintypes.LPVOID),
|
||||
("pSIPClientData", ctypes.wintypes.LPVOID),
|
||||
("dwUIChoice", ctypes.wintypes.DWORD),
|
||||
("fdwRevocationChecks", ctypes.wintypes.DWORD),
|
||||
("dwUnionChoice", ctypes.wintypes.DWORD),
|
||||
("u", WINTRUST_DATA_UNION),
|
||||
("dwStateAction", ctypes.wintypes.DWORD),
|
||||
("hWVTStateData", ctypes.wintypes.HANDLE),
|
||||
("pwszURLReference", ctypes.wintypes.LPCWSTR),
|
||||
("dwProvFlags", ctypes.wintypes.DWORD),
|
||||
("dwUIContext", ctypes.wintypes.DWORD),
|
||||
("pSignatureSettings", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class GUID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Data1", ctypes.wintypes.DWORD),
|
||||
("Data2", ctypes.wintypes.WORD),
|
||||
("Data3", ctypes.wintypes.WORD),
|
||||
("Data4", ctypes.c_ubyte * 8)
|
||||
]
|
||||
|
||||
V2_GUID = GUID(0x00AAC56B, 0xCD44, 0x11D0, (ctypes.c_ubyte * 8)(0x8C, 0xC2, 0x00, 0xC0, 0x4F, 0xC2, 0x95, 0xEE))
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def verify_signature(file_path):
|
||||
if os.name != 'nt': return 0
|
||||
try:
|
||||
wintrust = ctypes.windll.wintrust
|
||||
wintrust.WinVerifyTrust.argtypes = [ctypes.wintypes.HWND, ctypes.POINTER(GUID), ctypes.wintypes.LPVOID]
|
||||
wintrust.WinVerifyTrust.restype = ctypes.wintypes.LONG
|
||||
|
||||
abs_path = os.path.abspath(file_path)
|
||||
|
||||
fi = WINTRUST_FILE_INFO()
|
||||
fi.cbStruct = ctypes.sizeof(WINTRUST_FILE_INFO)
|
||||
fi.pcwszFilePath = abs_path
|
||||
fi.hFile = None
|
||||
fi.pgKnownSubject = None
|
||||
|
||||
wt_union = WINTRUST_DATA_UNION()
|
||||
wt_union.pFile = ctypes.pointer(fi)
|
||||
|
||||
td = WINTRUST_DATA()
|
||||
td.cbStruct = ctypes.sizeof(WINTRUST_DATA)
|
||||
td.pPolicyCallbackData = None
|
||||
td.pSIPClientData = None
|
||||
td.dwUIChoice = 2
|
||||
td.fdwRevocationChecks = 0
|
||||
td.dwUnionChoice = 1
|
||||
td.u = wt_union
|
||||
td.dwStateAction = 0
|
||||
td.hWVTStateData = None
|
||||
td.pwszURLReference = None
|
||||
td.dwProvFlags = 0
|
||||
td.dwUIContext = 0
|
||||
td.pSignatureSettings = None
|
||||
|
||||
return 1 if wintrust.WinVerifyTrust(None, ctypes.byref(V2_GUID), ctypes.byref(td)) == 0 else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class FeatureExtractor:
|
||||
_STRING_PATTERN = re.compile(b'[\x20-\x7E]{5,}')
|
||||
|
||||
_BYTE_HIST_KEYS = [f'ByteHist_{i:02X}' for i in range(256)]
|
||||
_BYTE_ENT_KEYS = [f'ByteEnt_{i:02X}' for i in range(256)]
|
||||
_STRING_KEYS = [f'StringHist_{i:02d}' for i in range(95)]
|
||||
_SECTION_HASH_KEYS = [f'SectionHash_{i:02d}' for i in range(50)]
|
||||
_CHAR_FLAGS = [0x00000020, 0x00000040, 0x00000080, 0x02000000, 0x20000000, 0x40000000, 0x80000000]
|
||||
_CHAR_COUNT_KEYS = {flag: f'Char_{flag:08X}_Count' for flag in _CHAR_FLAGS}
|
||||
_CHAR_ENT_KEYS = {flag: f'Char_{flag:08X}_MeanEntropy' for flag in _CHAR_FLAGS}
|
||||
|
||||
_C_LOG_C_TABLE = np.zeros(2049, dtype=np.float64)
|
||||
_C_LOG_C_TABLE[1:] = np.arange(1, 2049) * np.log2(np.arange(1, 2049))
|
||||
|
||||
@staticmethod
|
||||
def _safe_float(val):
|
||||
try:
|
||||
f = float(val)
|
||||
return f if not (math.isinf(f) or math.isnan(f)) else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def _calc_entropy(data):
|
||||
if not data: return 0.0
|
||||
arr = np.frombuffer(data, dtype=np.uint8)
|
||||
sz = len(arr)
|
||||
counts = np.bincount(arr, minlength=256)
|
||||
counts = counts[counts > 0]
|
||||
return float(np.log2(sz) - np.sum(counts * np.log2(counts)) / sz)
|
||||
|
||||
@classmethod
|
||||
def _extract_strings(cls, file_bytes):
|
||||
res = {"StringCount": 0.0, "StringMeanLength": 0.0, "StringEntropy": 0.0}
|
||||
res.update(dict.fromkeys(cls._STRING_KEYS, 0.0))
|
||||
|
||||
matches = cls._STRING_PATTERN.findall(file_bytes)
|
||||
if not matches:
|
||||
return res
|
||||
|
||||
combined_data = b''.join(matches)
|
||||
count = len(matches)
|
||||
total_len = len(combined_data)
|
||||
|
||||
res["StringCount"] = float(count)
|
||||
res["StringMeanLength"] = float(total_len) / count
|
||||
res["StringEntropy"] = cls._calc_entropy(combined_data)
|
||||
|
||||
arr = np.frombuffer(combined_data, dtype=np.uint8)
|
||||
counts = np.bincount(arr, minlength=127)
|
||||
char_counts = counts[0x20:0x7F]
|
||||
|
||||
res.update(dict(zip(cls._STRING_KEYS, char_counts.astype(np.float64) / total_len)))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def _extract_histograms(cls, file_bytes):
|
||||
arr = np.frombuffer(file_bytes, dtype=np.uint8)
|
||||
sz = len(arr)
|
||||
res = {}
|
||||
|
||||
if sz == 0:
|
||||
res.update(dict.fromkeys(cls._BYTE_HIST_KEYS, 0.0))
|
||||
res.update(dict.fromkeys(cls._BYTE_ENT_KEYS, 0.0))
|
||||
return res
|
||||
|
||||
byte_counts = np.bincount(arr, minlength=256)
|
||||
byte_hist = byte_counts.astype(np.float64) / sz
|
||||
res.update(dict(zip(cls._BYTE_HIST_KEYS, byte_hist)))
|
||||
|
||||
ent_hist = np.zeros(256, dtype=np.float64)
|
||||
window = 2048
|
||||
step = 1024
|
||||
|
||||
if sz < window:
|
||||
w_counts = byte_counts
|
||||
p = w_counts[w_counts > 0] / float(sz)
|
||||
entropy = float(-np.sum(p * np.log2(p)))
|
||||
ent_bin = min(int(entropy * 2.0), 15)
|
||||
byte_bins = w_counts.reshape(16, 16).sum(axis=1)
|
||||
idx_start = ent_bin * 16
|
||||
ent_hist[idx_start : idx_start+16] += byte_bins
|
||||
else:
|
||||
num_windows = (sz - window) // step + 1
|
||||
batch_windows = 10000
|
||||
|
||||
for w_start in range(0, num_windows, batch_windows):
|
||||
w_end = min(w_start + batch_windows, num_windows)
|
||||
b_num = w_end - w_start
|
||||
|
||||
start_byte = w_start * step
|
||||
end_byte = start_byte + b_num * step + step
|
||||
b_arr = arr[start_byte:end_byte]
|
||||
|
||||
arr_2d = b_arr.reshape(b_num + 1, step)
|
||||
offsets = (np.arange(b_num + 1, dtype=np.int32)[:, None] * 256)
|
||||
flat_idx = (arr_2d.astype(np.int32) + offsets).ravel()
|
||||
|
||||
chunk_counts = np.bincount(flat_idx, minlength=(b_num + 1) * 256).reshape(b_num + 1, 256)
|
||||
window_counts = chunk_counts[:-1] + chunk_counts[1:]
|
||||
|
||||
sum_c_log_c = np.sum(cls._C_LOG_C_TABLE[window_counts], axis=1)
|
||||
entropies = 11.0 - sum_c_log_c / 2048.0
|
||||
|
||||
ent_bins = (entropies * 2.0).astype(np.int32)
|
||||
np.clip(ent_bins, 0, 15, out=ent_bins)
|
||||
|
||||
byte_bins = window_counts.reshape(b_num, 16, 16).sum(axis=2)
|
||||
flat_ent_bins = (ent_bins[:, None] * 16 + np.arange(16)).ravel()
|
||||
np.add.at(ent_hist, flat_ent_bins, byte_bins.ravel())
|
||||
|
||||
ent_sum = np.sum(ent_hist)
|
||||
if ent_sum > 0:
|
||||
ent_hist /= ent_sum
|
||||
|
||||
res.update(dict(zip(cls._BYTE_ENT_KEYS, ent_hist)))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def _extract_overlay_features(cls, pe, fsize):
|
||||
overlay_offset = pe.get_overlay_data_start_offset()
|
||||
if not overlay_offset or overlay_offset >= fsize:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
overlay_data = pe.get_overlay()
|
||||
if not overlay_data:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
sz = len(overlay_data)
|
||||
return {
|
||||
"HasOverlay": 1.0,
|
||||
"OverlaySize": float(sz),
|
||||
"OverlayRatio": float(sz) / float(fsize),
|
||||
"OverlayEntropy": cls._calc_entropy(overlay_data)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_rich_header(cls, pe):
|
||||
if not hasattr(pe, 'RICH_HEADER') or not pe.RICH_HEADER:
|
||||
return {"HasRichHeader": 0.0, "RichHeaderCount": 0.0}
|
||||
return {
|
||||
"HasRichHeader": 1.0,
|
||||
"RichHeaderCount": float(len(pe.RICH_HEADER.values) // 2) if hasattr(pe.RICH_HEADER, 'values') else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_ep_anomalies(cls, pe):
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe, 'sections') or not pe.sections:
|
||||
return {"EntryPointSectionIndex": -1.0, "EntryPointInExecutable": 0.0, "EntryPointInLastSection": 0.0}
|
||||
|
||||
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
||||
ep_section_idx = -1
|
||||
ep_in_exec = 0.0
|
||||
|
||||
for idx, sec in enumerate(pe.sections):
|
||||
if sec.VirtualAddress <= ep < (sec.VirtualAddress + sec.Misc_VirtualSize):
|
||||
ep_section_idx = idx
|
||||
if sec.Characteristics & 0x20000000:
|
||||
ep_in_exec = 1.0
|
||||
break
|
||||
|
||||
return {
|
||||
"EntryPointSectionIndex": float(ep_section_idx),
|
||||
"EntryPointInExecutable": ep_in_exec,
|
||||
"EntryPointInLastSection": 1.0 if ep_section_idx == len(pe.sections) - 1 else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_advanced_resources(cls, pe):
|
||||
res_data = {
|
||||
"ResourceMaxEntropy": 0.0,
|
||||
"ResourceMinEntropy": 0.0,
|
||||
"ResourceMeanEntropy": 0.0,
|
||||
"ResourceLangCount": 0.0,
|
||||
"ResourceRCDataCount": 0.0
|
||||
}
|
||||
|
||||
if not hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
return res_data
|
||||
|
||||
entropies = []
|
||||
langs = set()
|
||||
rcdata_count = 0
|
||||
|
||||
for entry_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if hasattr(entry_type, 'id') and entry_type.id == 10:
|
||||
rcdata_count += len(getattr(entry_type.directory, 'entries', []))
|
||||
|
||||
if not hasattr(entry_type, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_id in entry_type.directory.entries:
|
||||
if not hasattr(entry_id, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_lang in entry_id.directory.entries:
|
||||
if hasattr(entry_lang, 'id'):
|
||||
langs.add(entry_lang.id)
|
||||
if hasattr(entry_lang, 'data'):
|
||||
try:
|
||||
data = pe.get_data(entry_lang.data.struct.OffsetToData, entry_lang.data.struct.Size)
|
||||
entropies.append(cls._calc_entropy(data))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
res_data["ResourceLangCount"] = float(len(langs))
|
||||
res_data["ResourceRCDataCount"] = float(rcdata_count)
|
||||
|
||||
if entropies:
|
||||
res_data["ResourceMaxEntropy"] = max(entropies)
|
||||
res_data["ResourceMinEntropy"] = min(entropies)
|
||||
res_data["ResourceMeanEntropy"] = sum(entropies) / len(entropies)
|
||||
|
||||
return res_data
|
||||
|
||||
@classmethod
|
||||
def _extract_load_config(cls, pe):
|
||||
cfg = {"HasLoadConfig": 0.0, "HasCFG": 0.0, "HasSEHTable": 0.0}
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_LOAD_CONFIG'):
|
||||
cfg["HasLoadConfig"] = 1.0
|
||||
struct = pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct
|
||||
if getattr(struct, 'GuardCFFunctionTable', 0) != 0:
|
||||
cfg["HasCFG"] = 1.0
|
||||
if getattr(struct, 'SEHandlerTable', 0) != 0:
|
||||
cfg["HasSEHTable"] = 1.0
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def _extract_security_directory(cls, pe, file_bytes, fsize):
|
||||
res = {"HasSignature": 0.0, "SignatureCount": 0.0}
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe.OPTIONAL_HEADER, 'DATA_DIRECTORY'):
|
||||
return res
|
||||
|
||||
sec_idx = 4
|
||||
directories = pe.OPTIONAL_HEADER.DATA_DIRECTORY
|
||||
if len(directories) <= sec_idx:
|
||||
return res
|
||||
|
||||
sec_dir = directories[sec_idx]
|
||||
if sec_dir.Size == 0 or sec_dir.VirtualAddress == 0:
|
||||
return res
|
||||
|
||||
res["HasSignature"] = 1.0
|
||||
offset = sec_dir.VirtualAddress
|
||||
size = sec_dir.Size
|
||||
|
||||
if offset + size > fsize or offset < 0 or size < 0:
|
||||
return res
|
||||
|
||||
count = 0
|
||||
curr = offset
|
||||
end = offset + size
|
||||
|
||||
while curr + 8 <= end:
|
||||
dw_length = int.from_bytes(file_bytes[curr:curr+4], 'little')
|
||||
if dw_length < 8:
|
||||
break
|
||||
count += 1
|
||||
curr += (dw_length + 7) & ~7
|
||||
|
||||
res["SignatureCount"] = float(count)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def extract(cls, file_bytes, file_path, fsize):
|
||||
if fsize == 0: return None
|
||||
base, dlls, apis, pe = {}, set(), set(), None
|
||||
|
||||
try:
|
||||
pe = pefile.PE(data=file_bytes, fast_load=True)
|
||||
|
||||
try:
|
||||
pe.parse_rich_header()
|
||||
pe.parse_data_directories(directories=[
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_DEBUG'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_TLS'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']
|
||||
])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base['TrustSigned'] = float(verify_signature(file_path))
|
||||
base['FileEntropy'] = cls._calc_entropy(file_bytes)
|
||||
base['FileSize'] = float(fsize)
|
||||
|
||||
fh = pe.FILE_HEADER
|
||||
base['Machine'] = getattr(fh, 'Machine', 0)
|
||||
base['NumberOfSections'] = getattr(fh, 'NumberOfSections', 0)
|
||||
base['TimeDateStamp'] = getattr(fh, 'TimeDateStamp', 0)
|
||||
base['PointerToSymbolTable'] = getattr(fh, 'PointerToSymbolTable', 0)
|
||||
base['NumberOfSymbols'] = getattr(fh, 'NumberOfSymbols', 0)
|
||||
base['SizeOfOptionalHeader'] = getattr(fh, 'SizeOfOptionalHeader', 0)
|
||||
base['Characteristics'] = getattr(fh, 'Characteristics', 0)
|
||||
|
||||
curr_ts = datetime.datetime.utcnow().timestamp()
|
||||
base['HasInvalidTimestamp'] = 1.0 if (base['TimeDateStamp'] < 631152000 or base['TimeDateStamp'] > curr_ts + 2592000) else 0.0
|
||||
base['FileTimeException'] = 1.0 if base['TimeDateStamp'] == 0 else 0.0
|
||||
base['Is64Bit'] = 1.0 if base['Machine'] in (0x8664, 0xAA64, 0x0200) else 0.0
|
||||
base['IsExe'] = 1.0 if pe.is_exe() else 0.0
|
||||
base['IsDll'] = 1.0 if pe.is_dll() else 0.0
|
||||
|
||||
base['ExceptionCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'OPTIONAL_HEADER'):
|
||||
op = pe.OPTIONAL_HEADER
|
||||
fields = ['Magic', 'MajorLinkerVersion', 'MinorLinkerVersion', 'SizeOfCode', 'SizeOfInitializedData', 'SizeOfUninitializedData', 'AddressOfEntryPoint', 'BaseOfCode', 'ImageBase', 'SectionAlignment', 'FileAlignment', 'MajorOperatingSystemVersion', 'MinorOperatingSystemVersion', 'MajorImageVersion', 'MinorImageVersion', 'MajorSubsystemVersion', 'MinorSubsystemVersion', 'SizeOfImage', 'SizeOfHeaders', 'CheckSum', 'Subsystem', 'DllCharacteristics', 'SizeOfStackReserve', 'SizeOfStackCommit', 'SizeOfHeapReserve', 'SizeOfHeapCommit', 'LoaderFlags', 'NumberOfRvaAndSizes']
|
||||
for f in fields: base[f] = getattr(op, f, 0)
|
||||
base['IsDriver'] = 1.0 if base.get('Subsystem') == 1 else 0.0
|
||||
|
||||
if hasattr(op, 'DATA_DIRECTORY'):
|
||||
for i, directory in enumerate(op.DATA_DIRECTORY):
|
||||
base[f'DataDirectory_{i}_Size'] = directory.Size
|
||||
base[f'DataDirectory_{i}_VA'] = directory.VirtualAddress
|
||||
|
||||
exc_idx = pefile.DIRECTORY_ENTRY.get('IMAGE_DIRECTORY_ENTRY_EXCEPTION', 3)
|
||||
if len(op.DATA_DIRECTORY) > exc_idx and op.DATA_DIRECTORY[exc_idx].Size > 0:
|
||||
base['ExceptionCount'] = op.DATA_DIRECTORY[exc_idx].Size // 12 if base['Machine'] in (0x8664, 0xAA64) else 0.0
|
||||
|
||||
res_flags_count = dict.fromkeys(cls._CHAR_COUNT_KEYS.values(), 0.0)
|
||||
res_flags_ent = dict.fromkeys(cls._CHAR_ENT_KEYS.values(), 0.0)
|
||||
res_sec_hash = dict.fromkeys(cls._SECTION_HASH_KEYS, 0.0)
|
||||
|
||||
if hasattr(pe, 'sections'):
|
||||
base['SectionCount'] = len(pe.sections)
|
||||
entropies, raw_sizes, v_sizes = [], [], []
|
||||
exec_sec, write_sec, read_sec, sec_exc = 0, 0, 0, 0
|
||||
|
||||
for section in pe.sections:
|
||||
sec_name = section.Name.rstrip(b'\x00')
|
||||
hash_val = zlib.crc32(sec_name) % 50
|
||||
res_sec_hash[cls._SECTION_HASH_KEYS[hash_val]] += 1.0
|
||||
|
||||
try:
|
||||
s_data = section.get_data()
|
||||
s_entropy = cls._calc_entropy(s_data)
|
||||
except Exception:
|
||||
s_entropy = 0.0
|
||||
|
||||
entropies.append(s_entropy)
|
||||
raw_sizes.append(section.SizeOfRawData)
|
||||
v_sizes.append(section.Misc_VirtualSize)
|
||||
|
||||
if section.Characteristics & 0x20000000: exec_sec += 1
|
||||
if section.Characteristics & 0x80000000: write_sec += 1
|
||||
if section.Characteristics & 0x40000000: read_sec += 1
|
||||
if section.SizeOfRawData + section.PointerToRawData > fsize: sec_exc = 1
|
||||
|
||||
for flag in cls._CHAR_FLAGS:
|
||||
if section.Characteristics & flag:
|
||||
res_flags_count[cls._CHAR_COUNT_KEYS[flag]] += 1.0
|
||||
res_flags_ent[cls._CHAR_ENT_KEYS[flag]] += s_entropy
|
||||
|
||||
base['SectionMaxEntropy'] = max(entropies) if entropies else 0.0
|
||||
base['SectionMinEntropy'] = min(entropies) if entropies else 0.0
|
||||
base['SectionMeanEntropy'] = sum(entropies) / len(entropies) if entropies else 0.0
|
||||
base['SectionMaxRawSize'] = max(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMinRawSize'] = min(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMeanRawSize'] = sum(raw_sizes) / len(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMaxVSize'] = max(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMinVSize'] = min(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMeanVSize'] = sum(v_sizes) / len(v_sizes) if v_sizes else 0.0
|
||||
base['ExecutableSections'] = float(exec_sec)
|
||||
base['WritableSections'] = float(write_sec)
|
||||
base['ReadableSections'] = float(read_sec)
|
||||
base['SectionException'] = float(sec_exc)
|
||||
|
||||
for flag in cls._CHAR_FLAGS:
|
||||
cnt_key = cls._CHAR_COUNT_KEYS[flag]
|
||||
if res_flags_count[cnt_key] > 0:
|
||||
res_flags_ent[cls._CHAR_ENT_KEYS[flag]] /= res_flags_count[cnt_key]
|
||||
|
||||
base.update(res_flags_count)
|
||||
base.update(res_flags_ent)
|
||||
base.update(res_sec_hash)
|
||||
|
||||
string_keys = ['FileDescription', 'FileVersion', 'ProductName', 'ProductVersion', 'CompanyName', 'LegalCopyright', 'Comments', 'InternalName', 'LegalTrademarks', 'SpecialBuild', 'PrivateBuild']
|
||||
for key in string_keys:
|
||||
base[f'{key}Length'] = 0.0
|
||||
|
||||
if hasattr(pe, 'FileInfo'):
|
||||
for fileinfo_list in pe.FileInfo:
|
||||
for fileinfo in fileinfo_list:
|
||||
if getattr(fileinfo, 'name', '') in ('StringFileInfo', b'StringFileInfo'):
|
||||
for st in getattr(fileinfo, 'StringTable', []):
|
||||
for key, val in st.entries.items():
|
||||
try:
|
||||
k = key.decode('utf-8', 'ignore') if isinstance(key, bytes) else str(key)
|
||||
if k in string_keys:
|
||||
v = val.decode('utf-8', 'ignore') if isinstance(val, bytes) else str(val)
|
||||
base[f'{k}Length'] = float(len(v))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_TLS') and hasattr(pe.DIRECTORY_ENTRY_TLS, 'struct'):
|
||||
base['HasTlsCallbacks'] = 1.0 if getattr(pe.DIRECTORY_ENTRY_TLS.struct, 'AddressOfCallBacks', 0) != 0 else 0.0
|
||||
|
||||
if hasattr(pe, 'VS_FIXEDFILEINFO') and len(pe.VS_FIXEDFILEINFO) > 0:
|
||||
flags = getattr(pe.VS_FIXEDFILEINFO[0], 'FileFlags', 0)
|
||||
base['IsDebug'] = 1.0 if flags & 0x1 else 0.0
|
||||
base['IsPreRelease'] = 1.0 if flags & 0x2 else 0.0
|
||||
base['IsPatched'] = 1.0 if flags & 0x4 else 0.0
|
||||
base['IsPrivateBuild'] = 1.0 if flags & 0x8 else 0.0
|
||||
base['IsSpecialBuild'] = 1.0 if flags & 0x20 else 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
|
||||
base['ImportCount'] = float(len(pe.DIRECTORY_ENTRY_IMPORT))
|
||||
func_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
||||
if getattr(entry, 'dll', None):
|
||||
try: dlls.add(entry.dll.decode('ascii', 'ignore').lower())
|
||||
except Exception: pass
|
||||
for imp in getattr(entry, 'imports', []):
|
||||
func_count += 1
|
||||
if getattr(imp, 'name', None):
|
||||
try: apis.add(imp.name.decode('ascii', 'ignore'))
|
||||
except Exception: pass
|
||||
base['ImportFunctionCount'] = float(func_count)
|
||||
|
||||
base['ExportCount'] = float(len(pe.DIRECTORY_ENTRY_EXPORT.symbols)) if (hasattr(pe, 'DIRECTORY_ENTRY_EXPORT') and hasattr(pe.DIRECTORY_ENTRY_EXPORT, 'symbols')) else 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
icon_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if getattr(entry, 'id', None) == 3 and hasattr(entry, 'directory'):
|
||||
icon_count += len(getattr(entry.directory, 'entries', []))
|
||||
base['IconCount'] = float(icon_count)
|
||||
else:
|
||||
base['IconCount'] = 0.0
|
||||
|
||||
base['DebugCount'] = float(len(pe.DIRECTORY_ENTRY_DEBUG)) if hasattr(pe, 'DIRECTORY_ENTRY_DEBUG') else 0.0
|
||||
|
||||
base.update(cls._extract_strings(file_bytes))
|
||||
base.update(cls._extract_histograms(file_bytes))
|
||||
base.update(cls._extract_overlay_features(pe, fsize))
|
||||
base.update(cls._extract_rich_header(pe))
|
||||
base.update(cls._extract_ep_anomalies(pe))
|
||||
base.update(cls._extract_advanced_resources(pe))
|
||||
base.update(cls._extract_load_config(pe))
|
||||
base.update(cls._extract_security_directory(pe, file_bytes, fsize))
|
||||
|
||||
for k in base: base[k] = cls._safe_float(base[k])
|
||||
return {"Base": base, "DLLs": list(dlls), "APIs": list(apis)}
|
||||
|
||||
except Exception: return None
|
||||
finally:
|
||||
if pe: pe.close()
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def load_existing_hashes():
|
||||
hashes = set()
|
||||
if not os.path.exists(JSONL_PATH):
|
||||
return hashes
|
||||
|
||||
pattern = re.compile(rb'"FileHash"\s*:\s*"([a-fA-F0-9]{64})"')
|
||||
with open(JSONL_PATH, 'rb') as f:
|
||||
for line in f:
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
hashes.add(match.group(1).decode('ascii'))
|
||||
return hashes
|
||||
|
||||
GLOBAL_HASHES = set()
|
||||
|
||||
def worker_init(shared_hashes):
|
||||
global GLOBAL_HASHES
|
||||
GLOBAL_HASHES = shared_hashes
|
||||
|
||||
def _extract_worker_wrapper(args):
|
||||
return _extract_worker(*args)
|
||||
|
||||
def _extract_worker(file_path, label):
|
||||
try:
|
||||
fsize = os.path.getsize(file_path)
|
||||
if fsize == 0 or fsize > MAX_FILE_SIZE:
|
||||
return 'error', None
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
sha256 = hashlib.sha256(file_bytes).hexdigest()
|
||||
|
||||
if sha256 in GLOBAL_HASHES:
|
||||
return 'skipped', None
|
||||
|
||||
res = FeatureExtractor.extract(file_bytes, file_path, fsize)
|
||||
if res:
|
||||
final_res = {"Label": label, "FileHash": sha256, **res}
|
||||
return 'success', (sha256, JSON_DUMPS(final_res) + "\n")
|
||||
|
||||
return 'error', None
|
||||
except Exception:
|
||||
return 'error', None
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def scan_and_save(path, label, existing_hashes):
|
||||
print(f"\n[*] Scanning path: {path} (Label={label})")
|
||||
files = [path] if os.path.isfile(path) else [os.path.join(r, f) for r, _, fs in os.walk(path) for f in fs]
|
||||
|
||||
total = len(files)
|
||||
if total == 0:
|
||||
print("[-] No files found.")
|
||||
return
|
||||
|
||||
print(f"[*] Found {total} files. Starting {MAX_WORKERS}-process extraction...\n")
|
||||
if HAS_ORJSON:
|
||||
print("[+] orjson acceleration enabled.")
|
||||
|
||||
count, errors, skipped, processed_files = 0, 0, 0, 0
|
||||
start_time = time.time()
|
||||
batch_buffer = []
|
||||
|
||||
pool = multiprocessing.Pool(processes=MAX_WORKERS, initializer=worker_init, initargs=(existing_hashes,))
|
||||
tasks = [(fpath, label) for fpath in files]
|
||||
|
||||
try:
|
||||
for status, data in pool.imap_unordered(_extract_worker_wrapper, tasks):
|
||||
processed_files += 1
|
||||
|
||||
if status == 'error':
|
||||
errors += 1
|
||||
elif status == 'skipped':
|
||||
skipped += 1
|
||||
elif status == 'success':
|
||||
sha256, json_str = data
|
||||
if sha256 not in existing_hashes:
|
||||
existing_hashes.add(sha256)
|
||||
batch_buffer.append(json_str)
|
||||
count += 1
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
if len(batch_buffer) >= BATCH_SIZE:
|
||||
with open(JSONL_PATH, 'a', encoding='utf-8') as f_out:
|
||||
f_out.writelines(batch_buffer)
|
||||
batch_buffer.clear()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
rate = processed_files / elapsed if elapsed > 0 else 0
|
||||
sys.stdout.write(f"\r[{processed_files}/{total}] Added: {count} | Skipped: {skipped} | Errors: {errors} | Speed: {rate:.1f} files/s")
|
||||
sys.stdout.flush()
|
||||
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[-] User aborted. Force terminating all child processes...")
|
||||
pool.terminate()
|
||||
pool.join()
|
||||
except Exception as e:
|
||||
print(f"\n[-] Critical error: {e}")
|
||||
pool.terminate()
|
||||
pool.join()
|
||||
finally:
|
||||
if batch_buffer:
|
||||
try:
|
||||
with open(JSONL_PATH, 'a', encoding='utf-8') as f_out:
|
||||
f_out.writelines(batch_buffer)
|
||||
except Exception as save_err:
|
||||
print(f"\n[-] Failed to save remaining buffer: {save_err}")
|
||||
|
||||
print(f"\n\n[+] Scan complete. Total added: {count}, Total skipped: {skipped}")
|
||||
|
||||
####################################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
print("\n---------------- PE Dataset Builder v4.0 ----------------\n")
|
||||
|
||||
existing_hashes = load_existing_hashes()
|
||||
print(f"[+] Found {len(existing_hashes)} existing hashes.\n")
|
||||
|
||||
try:
|
||||
while True:
|
||||
print("-" * 57)
|
||||
target_path = input("\n[*] Enter path (file or directory): ").strip().strip('"').strip("'")
|
||||
if target_path.lower() in ['exit', 'q']: break
|
||||
if not target_path or not os.path.exists(target_path):
|
||||
print("[-] Invalid path.")
|
||||
continue
|
||||
|
||||
while True:
|
||||
label_input = input("[*] Enter label (0=Safe, 1=Malware): ").strip()
|
||||
if label_input.lower() in ['exit', 'q']: sys.exit()
|
||||
if label_input in ['0', '1']: break
|
||||
print("[-] Please enter 0 or 1.")
|
||||
|
||||
scan_and_save(target_path, int(label_input), existing_hashes)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[-] Aborted.")
|
||||
@@ -0,0 +1,698 @@
|
||||
import os, re, sys, math, json, zlib, pefile, datetime
|
||||
import ctypes, ctypes.wintypes
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
####################################################################################################
|
||||
|
||||
MODEL_FILE = "Pefile_General_S1.onnx"
|
||||
FEATURE_FILE = "features.json"
|
||||
MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class WINTRUST_FILE_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbStruct", ctypes.wintypes.DWORD),
|
||||
("pcwszFilePath", ctypes.wintypes.LPCWSTR),
|
||||
("hFile", ctypes.wintypes.HANDLE),
|
||||
("pgKnownSubject", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA_UNION(ctypes.Union):
|
||||
_fields_ = [
|
||||
("pFile", ctypes.POINTER(WINTRUST_FILE_INFO)),
|
||||
("pCatalog", ctypes.wintypes.LPVOID),
|
||||
("pBlob", ctypes.wintypes.LPVOID),
|
||||
("pSgnr", ctypes.wintypes.LPVOID),
|
||||
("pCert", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbStruct", ctypes.wintypes.DWORD),
|
||||
("pPolicyCallbackData", ctypes.wintypes.LPVOID),
|
||||
("pSIPClientData", ctypes.wintypes.LPVOID),
|
||||
("dwUIChoice", ctypes.wintypes.DWORD),
|
||||
("fdwRevocationChecks", ctypes.wintypes.DWORD),
|
||||
("dwUnionChoice", ctypes.wintypes.DWORD),
|
||||
("u", WINTRUST_DATA_UNION),
|
||||
("dwStateAction", ctypes.wintypes.DWORD),
|
||||
("hWVTStateData", ctypes.wintypes.HANDLE),
|
||||
("pwszURLReference", ctypes.wintypes.LPCWSTR),
|
||||
("dwProvFlags", ctypes.wintypes.DWORD),
|
||||
("dwUIContext", ctypes.wintypes.DWORD),
|
||||
("pSignatureSettings", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class GUID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Data1", ctypes.wintypes.DWORD),
|
||||
("Data2", ctypes.wintypes.WORD),
|
||||
("Data3", ctypes.wintypes.WORD),
|
||||
("Data4", ctypes.c_ubyte * 8)
|
||||
]
|
||||
|
||||
V2_GUID = GUID(0x00AAC56B, 0xCD44, 0x11D0, (ctypes.c_ubyte * 8)(0x8C, 0xC2, 0x00, 0xC0, 0x4F, 0xC2, 0x95, 0xEE))
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class WinTrust:
|
||||
@staticmethod
|
||||
def verify(file_path):
|
||||
if os.name != 'nt':
|
||||
return 0
|
||||
try:
|
||||
wintrust = ctypes.windll.wintrust
|
||||
wintrust.WinVerifyTrust.argtypes = [ctypes.wintypes.HWND, ctypes.POINTER(GUID), ctypes.wintypes.LPVOID]
|
||||
wintrust.WinVerifyTrust.restype = ctypes.wintypes.LONG
|
||||
|
||||
fi = WINTRUST_FILE_INFO()
|
||||
fi.cbStruct = ctypes.sizeof(WINTRUST_FILE_INFO)
|
||||
fi.pcwszFilePath = os.path.abspath(file_path)
|
||||
fi.hFile = None
|
||||
fi.pgKnownSubject = None
|
||||
|
||||
wt_union = WINTRUST_DATA_UNION()
|
||||
wt_union.pFile = ctypes.pointer(fi)
|
||||
|
||||
td = WINTRUST_DATA()
|
||||
td.cbStruct = ctypes.sizeof(WINTRUST_DATA)
|
||||
td.pPolicyCallbackData = None
|
||||
td.pSIPClientData = None
|
||||
td.dwUIChoice = 2
|
||||
td.fdwRevocationChecks = 0
|
||||
td.dwUnionChoice = 1
|
||||
td.u = wt_union
|
||||
td.dwStateAction = 0
|
||||
td.hWVTStateData = None
|
||||
td.pwszURLReference = None
|
||||
td.dwProvFlags = 0
|
||||
td.dwUIContext = 0
|
||||
td.pSignatureSettings = None
|
||||
|
||||
return 1 if wintrust.WinVerifyTrust(None, ctypes.byref(V2_GUID), ctypes.byref(td)) == 0 else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class FeatureExtractor:
|
||||
_STRING_PATTERN = re.compile(b'[\x20-\x7E]{5,}')
|
||||
|
||||
_BYTE_HIST_KEYS = [f'ByteHist_{i:02X}' for i in range(256)]
|
||||
_BYTE_ENT_KEYS = [f'ByteEnt_{i:02X}' for i in range(256)]
|
||||
_STRING_KEYS = [f'StringHist_{i:02d}' for i in range(95)]
|
||||
_SECTION_HASH_KEYS = [f'SectionHash_{i:02d}' for i in range(50)]
|
||||
_CHAR_FLAGS = [0x00000020, 0x00000040, 0x00000080, 0x02000000, 0x20000000, 0x40000000, 0x80000000]
|
||||
_CHAR_COUNT_KEYS = {flag: f'Char_{flag:08X}_Count' for flag in _CHAR_FLAGS}
|
||||
_CHAR_ENT_KEYS = {flag: f'Char_{flag:08X}_MeanEntropy' for flag in _CHAR_FLAGS}
|
||||
|
||||
_C_LOG_C_TABLE = np.zeros(2049, dtype=np.float64)
|
||||
_C_LOG_C_TABLE[1:] = np.arange(1, 2049) * np.log2(np.arange(1, 2049))
|
||||
|
||||
@staticmethod
|
||||
def _safe_float(val):
|
||||
try:
|
||||
f = float(val)
|
||||
return f if not (math.isinf(f) or math.isnan(f)) else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def _calc_entropy(data):
|
||||
if not data: return 0.0
|
||||
arr = np.frombuffer(data, dtype=np.uint8)
|
||||
sz = len(arr)
|
||||
counts = np.bincount(arr, minlength=256)
|
||||
counts = counts[counts > 0]
|
||||
return float(np.log2(sz) - np.sum(counts * np.log2(counts)) / sz)
|
||||
|
||||
@classmethod
|
||||
def _extract_strings(cls, file_bytes):
|
||||
res = {"StringCount": 0.0, "StringMeanLength": 0.0, "StringEntropy": 0.0}
|
||||
res.update(dict.fromkeys(cls._STRING_KEYS, 0.0))
|
||||
|
||||
matches = cls._STRING_PATTERN.findall(file_bytes)
|
||||
if not matches:
|
||||
return res
|
||||
|
||||
combined_data = b''.join(matches)
|
||||
count = len(matches)
|
||||
total_len = len(combined_data)
|
||||
|
||||
res["StringCount"] = float(count)
|
||||
res["StringMeanLength"] = float(total_len) / count
|
||||
res["StringEntropy"] = cls._calc_entropy(combined_data)
|
||||
|
||||
arr = np.frombuffer(combined_data, dtype=np.uint8)
|
||||
counts = np.bincount(arr, minlength=127)
|
||||
char_counts = counts[0x20:0x7F]
|
||||
|
||||
res.update(dict(zip(cls._STRING_KEYS, char_counts.astype(np.float64) / total_len)))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def _extract_histograms(cls, file_bytes):
|
||||
arr = np.frombuffer(file_bytes, dtype=np.uint8)
|
||||
sz = len(arr)
|
||||
res = {}
|
||||
|
||||
if sz == 0:
|
||||
res.update(dict.fromkeys(cls._BYTE_HIST_KEYS, 0.0))
|
||||
res.update(dict.fromkeys(cls._BYTE_ENT_KEYS, 0.0))
|
||||
return res
|
||||
|
||||
byte_counts = np.bincount(arr, minlength=256)
|
||||
byte_hist = byte_counts.astype(np.float64) / sz
|
||||
res.update(dict(zip(cls._BYTE_HIST_KEYS, byte_hist)))
|
||||
|
||||
ent_hist = np.zeros(256, dtype=np.float64)
|
||||
window = 2048
|
||||
step = 1024
|
||||
|
||||
if sz < window:
|
||||
w_counts = byte_counts
|
||||
p = w_counts[w_counts > 0] / float(sz)
|
||||
entropy = float(-np.sum(p * np.log2(p)))
|
||||
ent_bin = min(int(entropy * 2.0), 15)
|
||||
byte_bins = w_counts.reshape(16, 16).sum(axis=1)
|
||||
idx_start = ent_bin * 16
|
||||
ent_hist[idx_start : idx_start+16] += byte_bins
|
||||
else:
|
||||
num_windows = (sz - window) // step + 1
|
||||
batch_windows = 10000
|
||||
|
||||
for w_start in range(0, num_windows, batch_windows):
|
||||
w_end = min(w_start + batch_windows, num_windows)
|
||||
b_num = w_end - w_start
|
||||
|
||||
start_byte = w_start * step
|
||||
end_byte = start_byte + b_num * step + step
|
||||
b_arr = arr[start_byte:end_byte]
|
||||
|
||||
arr_2d = b_arr.reshape(b_num + 1, step)
|
||||
offsets = (np.arange(b_num + 1, dtype=np.int32)[:, None] * 256)
|
||||
flat_idx = (arr_2d.astype(np.int32) + offsets).ravel()
|
||||
|
||||
chunk_counts = np.bincount(flat_idx, minlength=(b_num + 1) * 256).reshape(b_num + 1, 256)
|
||||
window_counts = chunk_counts[:-1] + chunk_counts[1:]
|
||||
|
||||
sum_c_log_c = np.sum(cls._C_LOG_C_TABLE[window_counts], axis=1)
|
||||
entropies = 11.0 - sum_c_log_c / 2048.0
|
||||
|
||||
ent_bins = (entropies * 2.0).astype(np.int32)
|
||||
np.clip(ent_bins, 0, 15, out=ent_bins)
|
||||
|
||||
byte_bins = window_counts.reshape(b_num, 16, 16).sum(axis=2)
|
||||
flat_ent_bins = (ent_bins[:, None] * 16 + np.arange(16)).ravel()
|
||||
np.add.at(ent_hist, flat_ent_bins, byte_bins.ravel())
|
||||
|
||||
ent_sum = np.sum(ent_hist)
|
||||
if ent_sum > 0:
|
||||
ent_hist /= ent_sum
|
||||
|
||||
res.update(dict(zip(cls._BYTE_ENT_KEYS, ent_hist)))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def _extract_overlay_features(cls, pe, fsize):
|
||||
overlay_offset = pe.get_overlay_data_start_offset()
|
||||
if not overlay_offset or overlay_offset >= fsize:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
overlay_data = pe.get_overlay()
|
||||
if not overlay_data:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
sz = len(overlay_data)
|
||||
return {
|
||||
"HasOverlay": 1.0,
|
||||
"OverlaySize": float(sz),
|
||||
"OverlayRatio": float(sz) / float(fsize),
|
||||
"OverlayEntropy": cls._calc_entropy(overlay_data)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_rich_header(cls, pe):
|
||||
if not hasattr(pe, 'RICH_HEADER') or not pe.RICH_HEADER:
|
||||
return {"HasRichHeader": 0.0, "RichHeaderCount": 0.0}
|
||||
return {
|
||||
"HasRichHeader": 1.0,
|
||||
"RichHeaderCount": float(len(pe.RICH_HEADER.values) // 2) if hasattr(pe.RICH_HEADER, 'values') else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_ep_anomalies(cls, pe):
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe, 'sections') or not pe.sections:
|
||||
return {"EntryPointSectionIndex": -1.0, "EntryPointInExecutable": 0.0, "EntryPointInLastSection": 0.0}
|
||||
|
||||
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
||||
ep_section_idx = -1
|
||||
ep_in_exec = 0.0
|
||||
|
||||
for idx, sec in enumerate(pe.sections):
|
||||
if sec.VirtualAddress <= ep < (sec.VirtualAddress + sec.Misc_VirtualSize):
|
||||
ep_section_idx = idx
|
||||
if sec.Characteristics & 0x20000000:
|
||||
ep_in_exec = 1.0
|
||||
break
|
||||
|
||||
return {
|
||||
"EntryPointSectionIndex": float(ep_section_idx),
|
||||
"EntryPointInExecutable": ep_in_exec,
|
||||
"EntryPointInLastSection": 1.0 if ep_section_idx == len(pe.sections) - 1 else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _extract_advanced_resources(cls, pe):
|
||||
res_data = {
|
||||
"ResourceMaxEntropy": 0.0,
|
||||
"ResourceMinEntropy": 0.0,
|
||||
"ResourceMeanEntropy": 0.0,
|
||||
"ResourceLangCount": 0.0,
|
||||
"ResourceRCDataCount": 0.0
|
||||
}
|
||||
|
||||
if not hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
return res_data
|
||||
|
||||
entropies = []
|
||||
langs = set()
|
||||
rcdata_count = 0
|
||||
|
||||
for entry_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if hasattr(entry_type, 'id') and entry_type.id == 10:
|
||||
rcdata_count += len(getattr(entry_type.directory, 'entries', []))
|
||||
|
||||
if not hasattr(entry_type, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_id in entry_type.directory.entries:
|
||||
if not hasattr(entry_id, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_lang in entry_id.directory.entries:
|
||||
if hasattr(entry_lang, 'id'):
|
||||
langs.add(entry_lang.id)
|
||||
if hasattr(entry_lang, 'data'):
|
||||
try:
|
||||
data = pe.get_data(entry_lang.data.struct.OffsetToData, entry_lang.data.struct.Size)
|
||||
entropies.append(cls._calc_entropy(data))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
res_data["ResourceLangCount"] = float(len(langs))
|
||||
res_data["ResourceRCDataCount"] = float(rcdata_count)
|
||||
|
||||
if entropies:
|
||||
res_data["ResourceMaxEntropy"] = max(entropies)
|
||||
res_data["ResourceMinEntropy"] = min(entropies)
|
||||
res_data["ResourceMeanEntropy"] = sum(entropies) / len(entropies)
|
||||
|
||||
return res_data
|
||||
|
||||
@classmethod
|
||||
def _extract_load_config(cls, pe):
|
||||
cfg = {"HasLoadConfig": 0.0, "HasCFG": 0.0, "HasSEHTable": 0.0}
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_LOAD_CONFIG'):
|
||||
cfg["HasLoadConfig"] = 1.0
|
||||
struct = pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct
|
||||
if getattr(struct, 'GuardCFFunctionTable', 0) != 0:
|
||||
cfg["HasCFG"] = 1.0
|
||||
if getattr(struct, 'SEHandlerTable', 0) != 0:
|
||||
cfg["HasSEHTable"] = 1.0
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def _extract_security_directory(cls, pe, file_bytes, fsize):
|
||||
res = {"HasSignature": 0.0, "SignatureCount": 0.0}
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe.OPTIONAL_HEADER, 'DATA_DIRECTORY'):
|
||||
return res
|
||||
|
||||
sec_idx = 4
|
||||
directories = pe.OPTIONAL_HEADER.DATA_DIRECTORY
|
||||
if len(directories) <= sec_idx:
|
||||
return res
|
||||
|
||||
sec_dir = directories[sec_idx]
|
||||
if sec_dir.Size == 0 or sec_dir.VirtualAddress == 0:
|
||||
return res
|
||||
|
||||
res["HasSignature"] = 1.0
|
||||
offset = sec_dir.VirtualAddress
|
||||
size = sec_dir.Size
|
||||
|
||||
if offset + size > fsize or offset < 0 or size < 0:
|
||||
return res
|
||||
|
||||
count = 0
|
||||
curr = offset
|
||||
end = offset + size
|
||||
|
||||
while curr + 8 <= end:
|
||||
dw_length = int.from_bytes(file_bytes[curr:curr+4], 'little')
|
||||
if dw_length < 8:
|
||||
break
|
||||
count += 1
|
||||
curr += (dw_length + 7) & ~7
|
||||
|
||||
res["SignatureCount"] = float(count)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def extract(cls, file_path):
|
||||
pe = None
|
||||
try:
|
||||
fsize = os.path.getsize(file_path)
|
||||
if fsize == 0 or fsize > MAX_FILE_SIZE:
|
||||
return None
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
base, dlls, apis = {}, set(), set()
|
||||
pe = pefile.PE(data=file_bytes, fast_load=True)
|
||||
|
||||
try:
|
||||
pe.parse_rich_header()
|
||||
pe.parse_data_directories(directories=[
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_DEBUG'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_TLS'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']
|
||||
])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base['TrustSigned'] = float(WinTrust.verify(file_path))
|
||||
base['FileEntropy'] = cls._calc_entropy(file_bytes)
|
||||
base['FileSize'] = float(fsize)
|
||||
|
||||
fh = pe.FILE_HEADER
|
||||
base['Machine'] = getattr(fh, 'Machine', 0)
|
||||
base['NumberOfSections'] = getattr(fh, 'NumberOfSections', 0)
|
||||
base['TimeDateStamp'] = getattr(fh, 'TimeDateStamp', 0)
|
||||
base['PointerToSymbolTable'] = getattr(fh, 'PointerToSymbolTable', 0)
|
||||
base['NumberOfSymbols'] = getattr(fh, 'NumberOfSymbols', 0)
|
||||
base['SizeOfOptionalHeader'] = getattr(fh, 'SizeOfOptionalHeader', 0)
|
||||
base['Characteristics'] = getattr(fh, 'Characteristics', 0)
|
||||
|
||||
curr_ts = datetime.datetime.utcnow().timestamp()
|
||||
base['HasInvalidTimestamp'] = 1.0 if (base['TimeDateStamp'] < 631152000 or base['TimeDateStamp'] > curr_ts + 2592000) else 0.0
|
||||
base['FileTimeException'] = 1.0 if base['TimeDateStamp'] == 0 else 0.0
|
||||
base['Is64Bit'] = 1.0 if base['Machine'] in (0x8664, 0xAA64, 0x0200) else 0.0
|
||||
base['IsExe'] = 1.0 if pe.is_exe() else 0.0
|
||||
base['IsDll'] = 1.0 if pe.is_dll() else 0.0
|
||||
|
||||
base['ExceptionCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'OPTIONAL_HEADER'):
|
||||
op = pe.OPTIONAL_HEADER
|
||||
fields = ['Magic', 'MajorLinkerVersion', 'MinorLinkerVersion', 'SizeOfCode', 'SizeOfInitializedData', 'SizeOfUninitializedData', 'AddressOfEntryPoint', 'BaseOfCode', 'ImageBase', 'SectionAlignment', 'FileAlignment', 'MajorOperatingSystemVersion', 'MinorOperatingSystemVersion', 'MajorImageVersion', 'MinorImageVersion', 'MajorSubsystemVersion', 'MinorSubsystemVersion', 'SizeOfImage', 'SizeOfHeaders', 'CheckSum', 'Subsystem', 'DllCharacteristics', 'SizeOfStackReserve', 'SizeOfStackCommit', 'SizeOfHeapReserve', 'SizeOfHeapCommit', 'LoaderFlags', 'NumberOfRvaAndSizes']
|
||||
for f in fields: base[f] = getattr(op, f, 0)
|
||||
base['IsDriver'] = 1.0 if base.get('Subsystem') == 1 else 0.0
|
||||
|
||||
if hasattr(op, 'DATA_DIRECTORY'):
|
||||
for i, directory in enumerate(op.DATA_DIRECTORY):
|
||||
base[f'DataDirectory_{i}_Size'] = directory.Size
|
||||
base[f'DataDirectory_{i}_VA'] = directory.VirtualAddress
|
||||
|
||||
exc_idx = pefile.DIRECTORY_ENTRY.get('IMAGE_DIRECTORY_ENTRY_EXCEPTION', 3)
|
||||
if len(op.DATA_DIRECTORY) > exc_idx and op.DATA_DIRECTORY[exc_idx].Size > 0:
|
||||
base['ExceptionCount'] = op.DATA_DIRECTORY[exc_idx].Size // 12 if base['Machine'] in (0x8664, 0xAA64) else 0.0
|
||||
|
||||
res_flags_count = dict.fromkeys(cls._CHAR_COUNT_KEYS.values(), 0.0)
|
||||
res_flags_ent = dict.fromkeys(cls._CHAR_ENT_KEYS.values(), 0.0)
|
||||
res_sec_hash = dict.fromkeys(cls._SECTION_HASH_KEYS, 0.0)
|
||||
|
||||
if hasattr(pe, 'sections'):
|
||||
base['SectionCount'] = len(pe.sections)
|
||||
entropies, raw_sizes, v_sizes = [], [], []
|
||||
exec_sec, write_sec, read_sec, sec_exc = 0, 0, 0, 0
|
||||
|
||||
for section in pe.sections:
|
||||
sec_name = section.Name.rstrip(b'\x00')
|
||||
hash_val = zlib.crc32(sec_name) % 50
|
||||
res_sec_hash[cls._SECTION_HASH_KEYS[hash_val]] += 1.0
|
||||
|
||||
try:
|
||||
s_data = section.get_data()
|
||||
s_entropy = cls._calc_entropy(s_data)
|
||||
except Exception:
|
||||
s_entropy = 0.0
|
||||
|
||||
entropies.append(s_entropy)
|
||||
raw_sizes.append(section.SizeOfRawData)
|
||||
v_sizes.append(section.Misc_VirtualSize)
|
||||
|
||||
if section.Characteristics & 0x20000000: exec_sec += 1
|
||||
if section.Characteristics & 0x80000000: write_sec += 1
|
||||
if section.Characteristics & 0x40000000: read_sec += 1
|
||||
if section.SizeOfRawData + section.PointerToRawData > fsize: sec_exc = 1
|
||||
|
||||
for flag in cls._CHAR_FLAGS:
|
||||
if section.Characteristics & flag:
|
||||
res_flags_count[cls._CHAR_COUNT_KEYS[flag]] += 1.0
|
||||
res_flags_ent[cls._CHAR_ENT_KEYS[flag]] += s_entropy
|
||||
|
||||
base['SectionMaxEntropy'] = max(entropies) if entropies else 0.0
|
||||
base['SectionMinEntropy'] = min(entropies) if entropies else 0.0
|
||||
base['SectionMeanEntropy'] = sum(entropies) / len(entropies) if entropies else 0.0
|
||||
base['SectionMaxRawSize'] = max(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMinRawSize'] = min(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMeanRawSize'] = sum(raw_sizes) / len(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMaxVSize'] = max(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMinVSize'] = min(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMeanVSize'] = sum(v_sizes) / len(v_sizes) if v_sizes else 0.0
|
||||
base['ExecutableSections'] = float(exec_sec)
|
||||
base['WritableSections'] = float(write_sec)
|
||||
base['ReadableSections'] = float(read_sec)
|
||||
base['SectionException'] = float(sec_exc)
|
||||
|
||||
for flag in cls._CHAR_FLAGS:
|
||||
cnt_key = cls._CHAR_COUNT_KEYS[flag]
|
||||
if res_flags_count[cnt_key] > 0:
|
||||
res_flags_ent[cls._CHAR_ENT_KEYS[flag]] /= res_flags_count[cnt_key]
|
||||
|
||||
base.update(res_flags_count)
|
||||
base.update(res_flags_ent)
|
||||
base.update(res_sec_hash)
|
||||
|
||||
string_keys = ['FileDescription', 'FileVersion', 'ProductName', 'ProductVersion', 'CompanyName', 'LegalCopyright', 'Comments', 'InternalName', 'LegalTrademarks', 'SpecialBuild', 'PrivateBuild']
|
||||
for key in string_keys:
|
||||
base[f'{key}Length'] = 0.0
|
||||
|
||||
if hasattr(pe, 'FileInfo'):
|
||||
for fileinfo_list in pe.FileInfo:
|
||||
for fileinfo in fileinfo_list:
|
||||
if getattr(fileinfo, 'name', '') in ('StringFileInfo', b'StringFileInfo'):
|
||||
for st in getattr(fileinfo, 'StringTable', []):
|
||||
for key, val in st.entries.items():
|
||||
try:
|
||||
k = key.decode('utf-8', 'ignore') if isinstance(key, bytes) else str(key)
|
||||
if k in string_keys:
|
||||
v = val.decode('utf-8', 'ignore') if isinstance(val, bytes) else str(val)
|
||||
base[f'{k}Length'] = float(len(v))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_TLS') and hasattr(pe.DIRECTORY_ENTRY_TLS, 'struct'):
|
||||
base['HasTlsCallbacks'] = 1.0 if getattr(pe.DIRECTORY_ENTRY_TLS.struct, 'AddressOfCallBacks', 0) != 0 else 0.0
|
||||
|
||||
if hasattr(pe, 'VS_FIXEDFILEINFO') and len(pe.VS_FIXEDFILEINFO) > 0:
|
||||
flags = getattr(pe.VS_FIXEDFILEINFO[0], 'FileFlags', 0)
|
||||
base['IsDebug'] = 1.0 if flags & 0x1 else 0.0
|
||||
base['IsPreRelease'] = 1.0 if flags & 0x2 else 0.0
|
||||
base['IsPatched'] = 1.0 if flags & 0x4 else 0.0
|
||||
base['IsPrivateBuild'] = 1.0 if flags & 0x8 else 0.0
|
||||
base['IsSpecialBuild'] = 1.0 if flags & 0x20 else 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
|
||||
base['ImportCount'] = float(len(pe.DIRECTORY_ENTRY_IMPORT))
|
||||
func_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
||||
if getattr(entry, 'dll', None):
|
||||
try: dlls.add(entry.dll.decode('ascii', 'ignore').lower())
|
||||
except Exception: pass
|
||||
for imp in getattr(entry, 'imports', []):
|
||||
func_count += 1
|
||||
if getattr(imp, 'name', None):
|
||||
try: apis.add(imp.name.decode('ascii', 'ignore'))
|
||||
except Exception: pass
|
||||
base['ImportFunctionCount'] = float(func_count)
|
||||
|
||||
base['ExportCount'] = float(len(pe.DIRECTORY_ENTRY_EXPORT.symbols)) if (hasattr(pe, 'DIRECTORY_ENTRY_EXPORT') and hasattr(pe.DIRECTORY_ENTRY_EXPORT, 'symbols')) else 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
icon_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if getattr(entry, 'id', None) == 3 and hasattr(entry, 'directory'):
|
||||
icon_count += len(getattr(entry.directory, 'entries', []))
|
||||
base['IconCount'] = float(icon_count)
|
||||
else:
|
||||
base['IconCount'] = 0.0
|
||||
|
||||
base['DebugCount'] = float(len(pe.DIRECTORY_ENTRY_DEBUG)) if hasattr(pe, 'DIRECTORY_ENTRY_DEBUG') else 0.0
|
||||
|
||||
base.update(cls._extract_strings(file_bytes))
|
||||
base.update(cls._extract_histograms(file_bytes))
|
||||
base.update(cls._extract_overlay_features(pe, fsize))
|
||||
base.update(cls._extract_rich_header(pe))
|
||||
base.update(cls._extract_ep_anomalies(pe))
|
||||
base.update(cls._extract_advanced_resources(pe))
|
||||
base.update(cls._extract_load_config(pe))
|
||||
base.update(cls._extract_security_directory(pe, file_bytes, fsize))
|
||||
|
||||
for k in base: base[k] = cls._safe_float(base[k])
|
||||
return {"Base": base, "DLLs": list(dlls), "APIs": list(apis)}
|
||||
|
||||
except Exception: return None
|
||||
finally:
|
||||
if pe: pe.close()
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class ModelPredictor:
|
||||
def __init__(self, model_path, feature_path):
|
||||
self.model_features = self._load_features(feature_path)
|
||||
self.sess = ort.InferenceSession(model_path, providers=['CPUExecutionProvider'])
|
||||
self.input_name = self.sess.get_inputs()[0].name
|
||||
|
||||
expected_dim = self.sess.get_inputs()[0].shape[1]
|
||||
current_dim = len(self.model_features)
|
||||
if current_dim != expected_dim:
|
||||
raise ValueError(f"Dimension mismatch! Model expects {expected_dim} features, but feature file lists {current_dim}.")
|
||||
|
||||
self.feat_map = {feat: i for i, feat in enumerate(self.model_features)}
|
||||
|
||||
def _load_features(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Feature file not found: {path}")
|
||||
with open(path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def predict(self, raw_data):
|
||||
vec = np.zeros((1, len(self.model_features)), dtype=np.float32)
|
||||
base = raw_data.get('Base', {})
|
||||
dlls = raw_data.get('DLLs', [])
|
||||
apis = raw_data.get('APIs', [])
|
||||
|
||||
for k, v in base.items():
|
||||
if k in self.feat_map:
|
||||
vec[0, self.feat_map[k]] = v
|
||||
|
||||
for d in dlls:
|
||||
h = zlib.crc32(d.encode('utf-8', 'ignore')) % 256
|
||||
feat_name = f"DllHash_{h:03d}"
|
||||
if feat_name in self.feat_map:
|
||||
vec[0, self.feat_map[feat_name]] += 1.0
|
||||
|
||||
for a in apis:
|
||||
h = zlib.crc32(a.encode('utf-8', 'ignore')) % 1024
|
||||
feat_name = f"ApiHash_{h:04d}"
|
||||
if feat_name in self.feat_map:
|
||||
vec[0, self.feat_map[feat_name]] += 1.0
|
||||
|
||||
outputs = self.sess.run(None, {self.input_name: vec})
|
||||
|
||||
if len(outputs) > 1:
|
||||
result = outputs[1]
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
prob_dict = result[0]
|
||||
if hasattr(prob_dict, 'get'):
|
||||
return float(prob_dict.get(1, prob_dict.get('1', 0.0)))
|
||||
elif isinstance(result, np.ndarray):
|
||||
if result.ndim == 2 and result.shape[1] > 1:
|
||||
return float(result[0][1])
|
||||
return 0.0
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def scan_target(target, predictor):
|
||||
files = []
|
||||
if os.path.isfile(target):
|
||||
files.append(target)
|
||||
elif os.path.isdir(target):
|
||||
for r, _, fs in os.walk(target):
|
||||
for f in fs:
|
||||
files.append(os.path.join(r, f))
|
||||
|
||||
if not files:
|
||||
print("[-] No valid PE files found.")
|
||||
return
|
||||
|
||||
print(f"\n[*] Scanning {len(files)} files...\n")
|
||||
print(f"{'RESULT':<10} | {'PROB':<8} | {'FILE'}")
|
||||
print("-" * 80)
|
||||
|
||||
safe_count = 0
|
||||
mal_count = 0
|
||||
|
||||
for fpath in files:
|
||||
try:
|
||||
data = FeatureExtractor.extract(fpath)
|
||||
if not data:
|
||||
print(f"{'ERROR':<10} | {'----':<8} | {fpath}")
|
||||
continue
|
||||
|
||||
prob = predictor.predict(data)
|
||||
is_malware = prob > 0.5
|
||||
|
||||
label = "MALWARE" if is_malware else "SAFE"
|
||||
if is_malware:
|
||||
mal_count += 1
|
||||
else:
|
||||
safe_count += 1
|
||||
|
||||
print(f"{label:<10} | {prob:.4f} | {fpath}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"{'FAIL':<10} | {'----':<8} | {fpath} ({str(e)})")
|
||||
|
||||
print("-" * 80)
|
||||
print(f"[*] Summary: Safe={safe_count}, Malware={mal_count}, Total={len(files)}")
|
||||
|
||||
####################################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n-------------------------- PE Malware Predictor v4.0 --------------------------\n")
|
||||
print(f"[*] © 2020-2026 87owo (PYAS Security)")
|
||||
print(f"[*] Loading model features and weights...")
|
||||
|
||||
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
||||
model_path = os.path.join(base_dir, MODEL_FILE)
|
||||
feature_path = os.path.join(base_dir, FEATURE_FILE)
|
||||
|
||||
try:
|
||||
predictor = ModelPredictor(model_path, feature_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Critical Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
for path in sys.argv[1:]:
|
||||
target = path.strip('"').strip("'")
|
||||
if os.path.exists(target):
|
||||
print()
|
||||
print("-" * 80)
|
||||
scan_target(target, predictor)
|
||||
else:
|
||||
print(f"[-] Path does not exist: {target}")
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
print()
|
||||
print("-" * 80)
|
||||
path = input("\n[?] Enter File or Folder Path (or 'q' to exit): ").strip().strip('"').strip("'")
|
||||
if path.lower() in ['q', 'exit']: break
|
||||
if not os.path.exists(path):
|
||||
print("[-] Path does not exist.")
|
||||
continue
|
||||
scan_target(path, predictor)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[-] Error: {e}")
|
||||
@@ -0,0 +1,306 @@
|
||||
import os, sys, time, json, zlib, array, onnxmltools, multiprocessing
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import lightgbm as lgb
|
||||
import scipy.sparse as sp
|
||||
|
||||
from collections import defaultdict
|
||||
from onnxmltools.convert.common.data_types import FloatTensorType
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, roc_auc_score
|
||||
|
||||
try:
|
||||
import orjson
|
||||
JSON_LOADS = orjson.loads
|
||||
except ImportError:
|
||||
JSON_LOADS = json.loads
|
||||
|
||||
####################################################################################################
|
||||
|
||||
JSONL_PATH = "pe_features.jsonl"
|
||||
MODEL_FILE = "model.txt"
|
||||
ONNX_FILE = "Pefile_General_S1.onnx"
|
||||
FEATURE_FILE = "features.json"
|
||||
TEST_SIZE = 0.0001
|
||||
RANDOM_SEED = 42
|
||||
|
||||
DLL_HASH_DIM = 256
|
||||
API_HASH_DIM = 1024
|
||||
|
||||
EXCLUDE_FEATURES = {
|
||||
'label', 'filehash',
|
||||
}
|
||||
|
||||
LGBM_PARAMS = {
|
||||
'objective': 'binary',
|
||||
'metric': ['binary_logloss', 'auc'],
|
||||
'boosting_type': 'gbdt',
|
||||
'num_leaves': 256,
|
||||
'feature_fraction': 0.8,
|
||||
'bagging_fraction': 1.0,
|
||||
'bagging_freq': 5,
|
||||
'verbose': -1,
|
||||
'seed': RANDOM_SEED,
|
||||
'n_jobs': -1,
|
||||
'max_depth': -1,
|
||||
'min_data_in_leaf': 50,
|
||||
}
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def analyze_schema(jsonl_path):
|
||||
print("[*] Stage 1: Scanning global schema...")
|
||||
base_keys = set()
|
||||
num_lines = 0
|
||||
|
||||
with open(jsonl_path, 'rb') as f:
|
||||
for line in f:
|
||||
num_lines += 1
|
||||
if not base_keys:
|
||||
try:
|
||||
data = JSON_LOADS(line)
|
||||
base_data = data.get('Base')
|
||||
if base_data:
|
||||
base_keys.update(base_data.keys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base_schema = sorted([k for k in base_keys if k.lower().strip() not in EXCLUDE_FEATURES])
|
||||
dll_schema = [f"DllHash_{i:03d}" for i in range(DLL_HASH_DIM)]
|
||||
api_schema = [f"ApiHash_{i:04d}" for i in range(API_HASH_DIM)]
|
||||
|
||||
return base_schema, dll_schema, api_schema, num_lines
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def load_data_efficiently(jsonl_path, base_schema, dll_schema, api_schema, num_lines):
|
||||
print(f"[*] Stage 2: Building sparse feature matrix dynamically via Multiprocessing...")
|
||||
final_features = base_schema + dll_schema + api_schema
|
||||
num_features = len(final_features)
|
||||
offset_dll = len(base_schema)
|
||||
offset_api = offset_dll + DLL_HASH_DIM
|
||||
|
||||
file_size = os.path.getsize(jsonl_path)
|
||||
workers = min(os.cpu_count() or 4, 16)
|
||||
chunk_size = file_size // workers
|
||||
|
||||
tasks = []
|
||||
for i in range(workers):
|
||||
start = i * chunk_size
|
||||
end = file_size if i == workers - 1 else (i + 1) * chunk_size
|
||||
tasks.append((jsonl_path, start, end, base_schema, num_features, offset_dll, offset_api))
|
||||
|
||||
with multiprocessing.Pool(workers) as pool:
|
||||
results = pool.map(_chunk_worker, tasks)
|
||||
|
||||
Xs = [r[0] for r in results if r[0].shape[0] > 0]
|
||||
ys = [r[1] for r in results if r[1].shape[0] > 0]
|
||||
|
||||
X_final = sp.vstack(Xs, format='csr') if Xs else sp.csr_matrix((0, num_features), dtype=np.float32)
|
||||
y_final = np.concatenate(ys) if ys else np.array([], dtype=np.int8)
|
||||
|
||||
return X_final, pd.Series(y_final), final_features
|
||||
|
||||
def _chunk_worker(args):
|
||||
filepath, start_byte, end_byte, base_schema, num_features, offset_dll, offset_api = args
|
||||
|
||||
base_idx = {feat: i for i, feat in enumerate(base_schema)}
|
||||
base_idx_get = base_idx.get
|
||||
|
||||
rows = array.array('i')
|
||||
cols = array.array('i')
|
||||
vals = array.array('f')
|
||||
y_list = array.array('b')
|
||||
|
||||
with open(filepath, 'rb') as f:
|
||||
if start_byte > 0:
|
||||
f.seek(start_byte - 1)
|
||||
if f.read(1) != b'\n':
|
||||
f.seek(start_byte)
|
||||
f.readline()
|
||||
else:
|
||||
f.seek(start_byte)
|
||||
else:
|
||||
f.seek(0)
|
||||
|
||||
row_idx = 0
|
||||
while f.tell() < end_byte:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
try:
|
||||
data = JSON_LOADS(line)
|
||||
y_list.append(data.get('Label', 0))
|
||||
|
||||
base_data = data.get('Base')
|
||||
if base_data:
|
||||
for k, v in base_data.items():
|
||||
idx = base_idx_get(k)
|
||||
if idx is not None:
|
||||
val = float(v)
|
||||
if val != 0.0:
|
||||
rows.append(row_idx)
|
||||
cols.append(idx)
|
||||
vals.append(val)
|
||||
|
||||
dll_data = data.get('DLLs')
|
||||
if dll_data:
|
||||
for d in dll_data:
|
||||
idx = zlib.crc32(d.encode('utf-8', 'ignore')) % DLL_HASH_DIM
|
||||
rows.append(row_idx)
|
||||
cols.append(offset_dll + idx)
|
||||
vals.append(1.0)
|
||||
|
||||
api_data = data.get('APIs')
|
||||
if api_data:
|
||||
for a in api_data:
|
||||
idx = zlib.crc32(a.encode('utf-8', 'ignore')) % API_HASH_DIM
|
||||
rows.append(row_idx)
|
||||
cols.append(offset_api + idx)
|
||||
vals.append(1.0)
|
||||
|
||||
row_idx += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
y_arr = np.frombuffer(y_list, dtype=np.int8)
|
||||
np_rows = np.frombuffer(rows, dtype=np.int32)
|
||||
np_cols = np.frombuffer(cols, dtype=np.int32)
|
||||
np_vals = np.frombuffer(vals, dtype=np.float32)
|
||||
|
||||
X_local = sp.csr_matrix((np_vals, (np_rows, np_cols)), shape=(row_idx, num_features), dtype=np.float32)
|
||||
|
||||
return X_local, y_arr
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def _calculate_dynamic_lr(current_iter: int) -> float:
|
||||
return max(0.01, 0.1 - (current_iter // 80) * 0.01)
|
||||
|
||||
def train_process(X, y, feature_names):
|
||||
print(f"[*] Dataset shape: {X.shape}")
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=y
|
||||
)
|
||||
|
||||
n_pos = (y_train == 1).sum()
|
||||
n_neg = (y_train == 0).sum()
|
||||
|
||||
base_weight = n_neg / n_pos if n_pos > 0 else 1.0
|
||||
weight_ratio_target = 0.005
|
||||
final_pos_weight = base_weight * weight_ratio_target
|
||||
|
||||
print(f"[*] Sample distribution: Safe={n_neg}, Malware={n_pos}")
|
||||
print(f"[*] Weight correction: Base={base_weight:.4f}, Final={final_pos_weight:.4f}")
|
||||
|
||||
train_data = lgb.Dataset(X_train, label=y_train, feature_name=feature_names)
|
||||
valid_data = lgb.Dataset(X_test, label=y_test, reference=train_data, feature_name=feature_names)
|
||||
|
||||
params = LGBM_PARAMS.copy()
|
||||
params['scale_pos_weight'] = final_pos_weight
|
||||
|
||||
print("\n[*] Starting LightGBM training...")
|
||||
start_time = time.time()
|
||||
|
||||
model = lgb.train(
|
||||
params,
|
||||
train_data,
|
||||
valid_sets=[valid_data],
|
||||
num_boost_round=500,
|
||||
callbacks=[
|
||||
lgb.log_evaluation(period=50),
|
||||
lgb.reset_parameter(learning_rate=_calculate_dynamic_lr)
|
||||
]
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n[+] Training complete, time elapsed {elapsed:.2f} seconds")
|
||||
|
||||
return model, X_test, y_test
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def export_onnx_model(model, feature_names):
|
||||
print(f"[*] Exporting ONNX model to {ONNX_FILE}...")
|
||||
try:
|
||||
initial_type = [('float_input', FloatTensorType([None, len(feature_names)]))]
|
||||
onnx_model = onnxmltools.convert_lightgbm(model, initial_types=initial_type)
|
||||
onnxmltools.utils.save_model(onnx_model, ONNX_FILE)
|
||||
|
||||
with open(FEATURE_FILE, 'w') as f:
|
||||
json.dump(feature_names, f)
|
||||
|
||||
print(f"[+] ONNX export successful. Feature order saved to {FEATURE_FILE}")
|
||||
except Exception as e:
|
||||
print(f"[-] ONNX export failed: {e}")
|
||||
|
||||
def evaluate_and_save(model, X_test, y_test):
|
||||
print("\n------------------- Evaluation Report -------------------\n")
|
||||
y_prob = model.predict(X_test, num_iteration=model.best_iteration)
|
||||
y_pred = (y_prob > 0.5).astype(int)
|
||||
|
||||
acc = accuracy_score(y_test, y_pred)
|
||||
auc = roc_auc_score(y_test, y_prob)
|
||||
|
||||
tp = np.sum((y_test == 1) & (y_pred == 1))
|
||||
fn = np.sum((y_test == 1) & (y_pred == 0))
|
||||
tn = np.sum((y_test == 0) & (y_pred == 0))
|
||||
fp = np.sum((y_test == 0) & (y_pred == 1))
|
||||
|
||||
total_malware = tp + fn
|
||||
total_safe = tn + fp
|
||||
|
||||
detection_rate = (tp / total_malware * 100) if total_malware > 0 else 0.0
|
||||
fpr = (fp / total_safe * 100) if total_safe > 0 else 0.0
|
||||
|
||||
print(f"Accuracy : {acc:.6f}")
|
||||
print(f"ROC AUC : {auc:.6f}\n")
|
||||
|
||||
print(f"True Positive (Class 1) : {detection_rate:.3f}% ({tp}/{total_malware})")
|
||||
print(f"False Positive (Class 0) : {fpr:.3f}% ({fp}/{total_safe})")
|
||||
|
||||
print("\n---------------------------------------------------------\n")
|
||||
print("[*] Top 20 Features:")
|
||||
|
||||
feature_names = model.feature_name()
|
||||
importance = model.feature_importance(importance_type='gain')
|
||||
feature_imp = pd.DataFrame(sorted(zip(importance, feature_names), reverse=True), columns=['Value', 'Feature'])
|
||||
print(feature_imp.head(20).to_string(index=False))
|
||||
|
||||
print("\n---------------------------------------------------------\n")
|
||||
|
||||
model.save_model(MODEL_FILE)
|
||||
print(f"[+] Native model saved to: {os.path.abspath(MODEL_FILE)}")
|
||||
|
||||
export_onnx_model(model, feature_names)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
print("\n---------------- PE Malware Trainer v4.0 ----------------\n")
|
||||
|
||||
if not os.path.exists(JSONL_PATH):
|
||||
print(f"[-] Error: Feature file {JSONL_PATH} not found.")
|
||||
sys.exit(1)
|
||||
|
||||
base_schema, dll_schema, api_schema, num_lines = analyze_schema(JSONL_PATH)
|
||||
if num_lines == 0:
|
||||
print("[-] Error: Feature file is empty.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[*] Detected Base features: {len(base_schema)}")
|
||||
print(f"[*] Hashing DLL features: {len(dll_schema)}")
|
||||
print(f"[*] Hashing API features: {len(api_schema)}")
|
||||
|
||||
X, y, final_features = load_data_efficiently(JSONL_PATH, base_schema, dll_schema, api_schema, num_lines)
|
||||
|
||||
try:
|
||||
model, X_test, y_test = train_process(X, y, final_features)
|
||||
evaluate_and_save(model, X_test, y_test)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[-] Training aborted.")
|
||||
except Exception as e:
|
||||
print(f"\n[-] System exception: {e}")
|
||||
@@ -0,0 +1,306 @@
|
||||
import os, sys, time, json, zlib, array, onnxmltools, multiprocessing
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import lightgbm as lgb
|
||||
import scipy.sparse as sp
|
||||
|
||||
from collections import defaultdict
|
||||
from onnxmltools.convert.common.data_types import FloatTensorType
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, roc_auc_score
|
||||
|
||||
try:
|
||||
import orjson
|
||||
JSON_LOADS = orjson.loads
|
||||
except ImportError:
|
||||
JSON_LOADS = json.loads
|
||||
|
||||
####################################################################################################
|
||||
|
||||
JSONL_PATH = "pe_features.jsonl"
|
||||
MODEL_FILE = "model.txt"
|
||||
ONNX_FILE = "Pefile_General_T1.onnx"
|
||||
FEATURE_FILE = "features.json"
|
||||
TEST_SIZE = 0.0001
|
||||
RANDOM_SEED = 42
|
||||
|
||||
DLL_HASH_DIM = 256
|
||||
API_HASH_DIM = 1024
|
||||
|
||||
EXCLUDE_FEATURES = {
|
||||
'label', 'filehash',
|
||||
}
|
||||
|
||||
LGBM_PARAMS = {
|
||||
'objective': 'binary',
|
||||
'metric': ['binary_logloss', 'auc'],
|
||||
'boosting_type': 'gbdt',
|
||||
'num_leaves': 128,
|
||||
'feature_fraction': 0.8,
|
||||
'bagging_fraction': 1.0,
|
||||
'bagging_freq': 5,
|
||||
'verbose': -1,
|
||||
'seed': RANDOM_SEED,
|
||||
'n_jobs': -1,
|
||||
'max_depth': -1,
|
||||
'min_data_in_leaf': 50,
|
||||
}
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def analyze_schema(jsonl_path):
|
||||
print("[*] Stage 1: Scanning global schema...")
|
||||
base_keys = set()
|
||||
num_lines = 0
|
||||
|
||||
with open(jsonl_path, 'rb') as f:
|
||||
for line in f:
|
||||
num_lines += 1
|
||||
if not base_keys:
|
||||
try:
|
||||
data = JSON_LOADS(line)
|
||||
base_data = data.get('Base')
|
||||
if base_data:
|
||||
base_keys.update(base_data.keys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base_schema = sorted([k for k in base_keys if k.lower().strip() not in EXCLUDE_FEATURES])
|
||||
dll_schema = [f"DllHash_{i:03d}" for i in range(DLL_HASH_DIM)]
|
||||
api_schema = [f"ApiHash_{i:04d}" for i in range(API_HASH_DIM)]
|
||||
|
||||
return base_schema, dll_schema, api_schema, num_lines
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def load_data_efficiently(jsonl_path, base_schema, dll_schema, api_schema, num_lines):
|
||||
print(f"[*] Stage 2: Building sparse feature matrix dynamically via Multiprocessing...")
|
||||
final_features = base_schema + dll_schema + api_schema
|
||||
num_features = len(final_features)
|
||||
offset_dll = len(base_schema)
|
||||
offset_api = offset_dll + DLL_HASH_DIM
|
||||
|
||||
file_size = os.path.getsize(jsonl_path)
|
||||
workers = min(os.cpu_count() or 4, 16)
|
||||
chunk_size = file_size // workers
|
||||
|
||||
tasks = []
|
||||
for i in range(workers):
|
||||
start = i * chunk_size
|
||||
end = file_size if i == workers - 1 else (i + 1) * chunk_size
|
||||
tasks.append((jsonl_path, start, end, base_schema, num_features, offset_dll, offset_api))
|
||||
|
||||
with multiprocessing.Pool(workers) as pool:
|
||||
results = pool.map(_chunk_worker, tasks)
|
||||
|
||||
Xs = [r[0] for r in results if r[0].shape[0] > 0]
|
||||
ys = [r[1] for r in results if r[1].shape[0] > 0]
|
||||
|
||||
X_final = sp.vstack(Xs, format='csr') if Xs else sp.csr_matrix((0, num_features), dtype=np.float32)
|
||||
y_final = np.concatenate(ys) if ys else np.array([], dtype=np.int8)
|
||||
|
||||
return X_final, pd.Series(y_final), final_features
|
||||
|
||||
def _chunk_worker(args):
|
||||
filepath, start_byte, end_byte, base_schema, num_features, offset_dll, offset_api = args
|
||||
|
||||
base_idx = {feat: i for i, feat in enumerate(base_schema)}
|
||||
base_idx_get = base_idx.get
|
||||
|
||||
rows = array.array('i')
|
||||
cols = array.array('i')
|
||||
vals = array.array('f')
|
||||
y_list = array.array('b')
|
||||
|
||||
with open(filepath, 'rb') as f:
|
||||
if start_byte > 0:
|
||||
f.seek(start_byte - 1)
|
||||
if f.read(1) != b'\n':
|
||||
f.seek(start_byte)
|
||||
f.readline()
|
||||
else:
|
||||
f.seek(start_byte)
|
||||
else:
|
||||
f.seek(0)
|
||||
|
||||
row_idx = 0
|
||||
while f.tell() < end_byte:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
try:
|
||||
data = JSON_LOADS(line)
|
||||
y_list.append(data.get('Label', 0))
|
||||
|
||||
base_data = data.get('Base')
|
||||
if base_data:
|
||||
for k, v in base_data.items():
|
||||
idx = base_idx_get(k)
|
||||
if idx is not None:
|
||||
val = float(v)
|
||||
if val != 0.0:
|
||||
rows.append(row_idx)
|
||||
cols.append(idx)
|
||||
vals.append(val)
|
||||
|
||||
dll_data = data.get('DLLs')
|
||||
if dll_data:
|
||||
for d in dll_data:
|
||||
idx = zlib.crc32(d.encode('utf-8', 'ignore')) % DLL_HASH_DIM
|
||||
rows.append(row_idx)
|
||||
cols.append(offset_dll + idx)
|
||||
vals.append(1.0)
|
||||
|
||||
api_data = data.get('APIs')
|
||||
if api_data:
|
||||
for a in api_data:
|
||||
idx = zlib.crc32(a.encode('utf-8', 'ignore')) % API_HASH_DIM
|
||||
rows.append(row_idx)
|
||||
cols.append(offset_api + idx)
|
||||
vals.append(1.0)
|
||||
|
||||
row_idx += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
y_arr = np.frombuffer(y_list, dtype=np.int8)
|
||||
np_rows = np.frombuffer(rows, dtype=np.int32)
|
||||
np_cols = np.frombuffer(cols, dtype=np.int32)
|
||||
np_vals = np.frombuffer(vals, dtype=np.float32)
|
||||
|
||||
X_local = sp.csr_matrix((np_vals, (np_rows, np_cols)), shape=(row_idx, num_features), dtype=np.float32)
|
||||
|
||||
return X_local, y_arr
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def _calculate_dynamic_lr(current_iter: int) -> float:
|
||||
return max(0.01, 0.1 - (current_iter // 80) * 0.01)
|
||||
|
||||
def train_process(X, y, feature_names):
|
||||
print(f"[*] Dataset shape: {X.shape}")
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=y
|
||||
)
|
||||
|
||||
n_pos = (y_train == 1).sum()
|
||||
n_neg = (y_train == 0).sum()
|
||||
|
||||
base_weight = n_neg / n_pos if n_pos > 0 else 1.0
|
||||
weight_ratio_target = 0.001
|
||||
final_pos_weight = base_weight * weight_ratio_target
|
||||
|
||||
print(f"[*] Sample distribution: Safe={n_neg}, Malware={n_pos}")
|
||||
print(f"[*] Weight correction: Base={base_weight:.4f}, Final={final_pos_weight:.4f}")
|
||||
|
||||
train_data = lgb.Dataset(X_train, label=y_train, feature_name=feature_names)
|
||||
valid_data = lgb.Dataset(X_test, label=y_test, reference=train_data, feature_name=feature_names)
|
||||
|
||||
params = LGBM_PARAMS.copy()
|
||||
params['scale_pos_weight'] = final_pos_weight
|
||||
|
||||
print("\n[*] Starting LightGBM training...")
|
||||
start_time = time.time()
|
||||
|
||||
model = lgb.train(
|
||||
params,
|
||||
train_data,
|
||||
valid_sets=[valid_data],
|
||||
num_boost_round=500,
|
||||
callbacks=[
|
||||
lgb.log_evaluation(period=50),
|
||||
lgb.reset_parameter(learning_rate=_calculate_dynamic_lr)
|
||||
]
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n[+] Training complete, time elapsed {elapsed:.2f} seconds")
|
||||
|
||||
return model, X_test, y_test
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def export_onnx_model(model, feature_names):
|
||||
print(f"[*] Exporting ONNX model to {ONNX_FILE}...")
|
||||
try:
|
||||
initial_type = [('float_input', FloatTensorType([None, len(feature_names)]))]
|
||||
onnx_model = onnxmltools.convert_lightgbm(model, initial_types=initial_type)
|
||||
onnxmltools.utils.save_model(onnx_model, ONNX_FILE)
|
||||
|
||||
with open(FEATURE_FILE, 'w') as f:
|
||||
json.dump(feature_names, f)
|
||||
|
||||
print(f"[+] ONNX export successful. Feature order saved to {FEATURE_FILE}")
|
||||
except Exception as e:
|
||||
print(f"[-] ONNX export failed: {e}")
|
||||
|
||||
def evaluate_and_save(model, X_test, y_test):
|
||||
print("\n------------------- Evaluation Report -------------------\n")
|
||||
y_prob = model.predict(X_test, num_iteration=model.best_iteration)
|
||||
y_pred = (y_prob > 0.5).astype(int)
|
||||
|
||||
acc = accuracy_score(y_test, y_pred)
|
||||
auc = roc_auc_score(y_test, y_prob)
|
||||
|
||||
tp = np.sum((y_test == 1) & (y_pred == 1))
|
||||
fn = np.sum((y_test == 1) & (y_pred == 0))
|
||||
tn = np.sum((y_test == 0) & (y_pred == 0))
|
||||
fp = np.sum((y_test == 0) & (y_pred == 1))
|
||||
|
||||
total_malware = tp + fn
|
||||
total_safe = tn + fp
|
||||
|
||||
detection_rate = (tp / total_malware * 100) if total_malware > 0 else 0.0
|
||||
fpr = (fp / total_safe * 100) if total_safe > 0 else 0.0
|
||||
|
||||
print(f"Accuracy : {acc:.6f}")
|
||||
print(f"ROC AUC : {auc:.6f}\n")
|
||||
|
||||
print(f"True Positive (Class 1) : {detection_rate:.3f}% ({tp}/{total_malware})")
|
||||
print(f"False Positive (Class 0) : {fpr:.3f}% ({fp}/{total_safe})")
|
||||
|
||||
print("\n---------------------------------------------------------\n")
|
||||
print("[*] Top 20 Features:")
|
||||
|
||||
feature_names = model.feature_name()
|
||||
importance = model.feature_importance(importance_type='gain')
|
||||
feature_imp = pd.DataFrame(sorted(zip(importance, feature_names), reverse=True), columns=['Value', 'Feature'])
|
||||
print(feature_imp.head(20).to_string(index=False))
|
||||
|
||||
print("\n---------------------------------------------------------\n")
|
||||
|
||||
model.save_model(MODEL_FILE)
|
||||
print(f"[+] Native model saved to: {os.path.abspath(MODEL_FILE)}")
|
||||
|
||||
export_onnx_model(model, feature_names)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
print("\n---------------- PE Malware Trainer v4.0 ----------------\n")
|
||||
|
||||
if not os.path.exists(JSONL_PATH):
|
||||
print(f"[-] Error: Feature file {JSONL_PATH} not found.")
|
||||
sys.exit(1)
|
||||
|
||||
base_schema, dll_schema, api_schema, num_lines = analyze_schema(JSONL_PATH)
|
||||
if num_lines == 0:
|
||||
print("[-] Error: Feature file is empty.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[*] Detected Base features: {len(base_schema)}")
|
||||
print(f"[*] Hashing DLL features: {len(dll_schema)}")
|
||||
print(f"[*] Hashing API features: {len(api_schema)}")
|
||||
|
||||
X, y, final_features = load_data_efficiently(JSONL_PATH, base_schema, dll_schema, api_schema, num_lines)
|
||||
|
||||
try:
|
||||
model, X_test, y_test = train_process(X, y, final_features)
|
||||
evaluate_and_save(model, X_test, y_test)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[-] Training aborted.")
|
||||
except Exception as e:
|
||||
print(f"\n[-] System exception: {e}")
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,841 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-window: rgb(245, 245, 247);
|
||||
--bg-nav: rgb(255, 255, 255);
|
||||
--bg-panel: rgb(255, 255, 255);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: rgb(29, 29, 31);
|
||||
--text-secondary: rgb(134, 134, 139);
|
||||
--border-color: rgb(210, 210, 215);
|
||||
--accent-color: rgb(0, 122, 255);
|
||||
--accent-alpha: rgba(0, 122, 255, 0.1);
|
||||
--accent-shadow: rgba(0, 122, 255, 0.2);
|
||||
--accent-hover: rgba(0, 122, 255, 0.3);
|
||||
--shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-window);
|
||||
}
|
||||
|
||||
.navigate-window {
|
||||
height: 44px;
|
||||
background-color: var(--bg-nav);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px 0 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.navigate-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
width: 46px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.15s;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ctrl-btn:hover { background-color: var(--bg-hover); color: var(--text-primary); }
|
||||
#close_button:hover { background-color: #ff3b30; color: white; }
|
||||
|
||||
.theme-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: flex;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.options-window {
|
||||
width: 64px;
|
||||
background-color: var(--bg-nav);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
gap: 12px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-btn svg { width: 22px; height: 22px; }
|
||||
.nav-btn:hover { background-color: var(--bg-hover); color: var(--text-primary); }
|
||||
.nav-btn.active { background-color: var(--accent-alpha); color: var(--accent-color); }
|
||||
|
||||
.spacer { flex-grow: 1; }
|
||||
|
||||
.content-window {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 28px 32px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(8px);
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.state-widget {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.state-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.log-widget {
|
||||
flex-grow: 1;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.license-widget {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.log-text, .license-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-panel);
|
||||
color: var(--text-primary);
|
||||
resize: none;
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.progress-widget {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-group h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-group p {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.modern-action-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.modern-select:hover, .modern-select:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
|
||||
}
|
||||
|
||||
.modern-btn {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background-color: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modern-btn:hover { background-color: var(--bg-hover); }
|
||||
|
||||
.primary-btn {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px var(--accent-shadow);
|
||||
}
|
||||
.primary-btn:hover {
|
||||
box-shadow: 0 4px 8px var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
background-color: var(--accent-color);
|
||||
opacity: 0.95;
|
||||
}
|
||||
.primary-btn:active {
|
||||
transform: translateY(0);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.danger-btn { color: #ff3b30; border-color: #ff3b30; }
|
||||
.danger-btn:hover { background-color: rgba(255, 59, 48, 0.1); }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.scan-content {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.virus-widget {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-panel);
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.virus-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.list-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px 24px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.list-item[data-target]:hover, .list-item[data-action]:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.item-info h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-info p {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
}
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: var(--border-color);
|
||||
transition: .3s;
|
||||
border-radius: 28px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 24px; width: 24px;
|
||||
left: 2px; bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .3s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.toggle-switch input:checked + .slider {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.toggle-switch input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.icon-action-btn {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.icon-action-btn:hover { background-color: var(--bg-hover); color: var(--text-primary); }
|
||||
.icon-action-btn svg { width: 20px; height: 20px; }
|
||||
|
||||
.sub-header-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-header-toolbar h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.icon-btn:hover { background-color: var(--bg-hover); color: var(--text-primary); }
|
||||
.icon-btn svg { width: 24px; height: 24px; }
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0 14px;
|
||||
width: 200px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.search-box:focus-within {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px var(--accent-shadow);
|
||||
}
|
||||
|
||||
.search-box svg {
|
||||
width: 18px; height: 18px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
padding: 10px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.manage-widget {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-panel);
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.manage-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.manage-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.manage-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.manage-list-item:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.manage-list-item input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent-color);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.manage-list-item-content {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-select-wrapper {
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
display: inline-grid;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
}
|
||||
select.hidden + .custom-select-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
.custom-select-trigger {
|
||||
grid-area: 1 / 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-panel);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.custom-select-phantom {
|
||||
grid-area: 1 / 1;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
display: grid;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.custom-select-phantom span {
|
||||
grid-area: 1 / 1;
|
||||
white-space: nowrap;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.custom-select-wrapper.open .custom-select-trigger,
|
||||
.custom-select-trigger:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px var(--accent-shadow);
|
||||
}
|
||||
.custom-select-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.custom-select-wrapper.open .custom-select-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.custom-select-icon svg { width: 16px; height: 16px; }
|
||||
.custom-select-options {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
|
||||
background-color: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
z-index: 100;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
max-height: 0;
|
||||
padding: 0 6px;
|
||||
border-width: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0.2s, max-height 0s 0.2s, padding 0s 0.2s, border-width 0s 0.2s, box-shadow 0s 0.2s;
|
||||
}
|
||||
.custom-select-wrapper.open .custom-select-options {
|
||||
max-height: 260px;
|
||||
padding: 6px;
|
||||
border-width: 1px;
|
||||
overflow-y: auto;
|
||||
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0s, max-height 0s 0s, padding 0s 0s, border-width 0s 0s, box-shadow 0s 0s;
|
||||
}
|
||||
.custom-select-option {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
.custom-select-option:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
.custom-select-option.selected {
|
||||
background-color: var(--accent-alpha);
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.home-top-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loading_overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.4s ease, visibility 0.4s ease;
|
||||
}
|
||||
|
||||
#loading_overlay.fade-out {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: rotate 2s linear infinite;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.spinner .path {
|
||||
stroke: #86868b;
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
||||
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
|
||||
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
||||
}
|
||||
|
||||
.app-container.fade-in {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.manage-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--bg-panel);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.manage-list-header input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent-color);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-cols-container, .manage-list-item-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-header {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
background-color: var(--bg-panel);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.col-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.manage-list-header, .col-header, .col-resizer {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.col-resizer {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.col-resizer::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.col-resizer:hover::after,
|
||||
.col-resizer.resizing::after {
|
||||
background-color: var(--text-secondary) !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
body.resizing-active,
|
||||
body.resizing-active * {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
.col-resizer:hover {
|
||||
background-color: var(--accent-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.col-header:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
font-size: 10px;
|
||||
margin-left: 4px;
|
||||
color: var(--accent-color);
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.row-col {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.custom-context-menu {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
background-color: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 4px;
|
||||
display: none;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.custom-context-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.custom-context-menu-item:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.custom-context-menu-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,977 @@
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
document.addEventListener(eventName, e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const { dict, langMap } = window.AppI18n;
|
||||
|
||||
const appState = {
|
||||
lang: "english_switch",
|
||||
theme: "system_switch",
|
||||
scanning: false,
|
||||
firstLaunch: false,
|
||||
taskmgrTimer: null,
|
||||
taskmgrActive: false,
|
||||
virusMap: new Map(),
|
||||
virusResults: [],
|
||||
junkList: []
|
||||
};
|
||||
|
||||
const reverseI18nMap = Object.entries(dict["traditional_switch"]).reduce((acc, [key, val]) => {
|
||||
acc[val] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const initI18nKeys = () => {
|
||||
document.querySelectorAll("[data-i18n]").forEach(el => {
|
||||
let key = el.getAttribute('data-i18n');
|
||||
if (!key) {
|
||||
const text = el.textContent.trim();
|
||||
key = reverseI18nMap[text] || text;
|
||||
el.setAttribute('data-i18n', key);
|
||||
}
|
||||
el.dataset.originText = el.textContent.trim();
|
||||
});
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
|
||||
let key = el.getAttribute('data-i18n-placeholder');
|
||||
if (reverseI18nMap[key]) {
|
||||
el.setAttribute('data-i18n-placeholder', reverseI18nMap[key]);
|
||||
}
|
||||
el.dataset.originPlaceholder = el.getAttribute('placeholder');
|
||||
});
|
||||
document.querySelectorAll("option[data-i18n]").forEach(el => {
|
||||
let key = el.getAttribute('data-i18n');
|
||||
if (!key) {
|
||||
const text = el.textContent.trim();
|
||||
key = reverseI18nMap[text] || text;
|
||||
el.setAttribute('data-i18n', key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.custom-select-wrapper').forEach(w => w.classList.remove('open'));
|
||||
});
|
||||
|
||||
const getMsg = (key) => (dict[appState.lang] || dict["english_switch"])[key] || key;
|
||||
|
||||
const formatMsg = (key, ...args) => {
|
||||
let msg = getMsg(key);
|
||||
args.forEach((arg, i) => msg = msg.split(`{${i}}`).join(arg));
|
||||
return msg;
|
||||
};
|
||||
|
||||
const escapeHtml = (unsafe) => {
|
||||
if (typeof unsafe !== 'string') return unsafe;
|
||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
};
|
||||
|
||||
const themes = {
|
||||
"white_switch": { "--bg-window": "245, 245, 247", "--bg-nav": "255, 255, 255", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(0,0,0,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"210, 210, 215", "--accent-color":"0, 122, 255", "--accent-alpha":"rgba(0, 122, 255, 0.1)", "--accent-shadow":"rgba(0, 122, 255, 0.2)", "--accent-hover":"rgba(0, 122, 255, 0.3)" },
|
||||
"black_switch": { "--bg-window": "0, 0, 0", "--bg-nav": "28, 28, 30", "--bg-panel": "28, 28, 30", "--bg-hover": "rgba(255,255,255,0.1)", "--text-primary": "245, 245, 247", "--text-secondary": "134, 134, 139", "--border-color":"44, 44, 46", "--accent-color":"10, 132, 255", "--accent-alpha":"rgba(10, 132, 255, 0.1)", "--accent-shadow":"rgba(10, 132, 255, 0.2)", "--accent-hover":"rgba(10, 132, 255, 0.3)" },
|
||||
"red_switch": { "--bg-window": "253, 246, 246", "--bg-nav": "255, 240, 240", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(255,59,48,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"250, 220, 220", "--accent-color":"255, 59, 48", "--accent-alpha":"rgba(255, 59, 48, 0.1)", "--accent-shadow":"rgba(255, 59, 48, 0.2)", "--accent-hover":"rgba(255, 59, 48, 0.3)" },
|
||||
"yellow_switch": { "--bg-window": "254, 251, 243", "--bg-nav": "255, 248, 230", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(255,149,0,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"250, 240, 215", "--accent-color":"255, 149, 0", "--accent-alpha":"rgba(255, 149, 0, 0.1)", "--accent-shadow":"rgba(255, 149, 0, 0.2)", "--accent-hover":"rgba(255, 149, 0, 0.3)" },
|
||||
"green_switch": { "--bg-window": "246, 253, 248", "--bg-nav": "235, 250, 240", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(52,199,89,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"220, 240, 225", "--accent-color":"52, 199, 89", "--accent-alpha":"rgba(52, 199, 89, 0.1)", "--accent-shadow":"rgba(52, 199, 89, 0.2)", "--accent-hover":"rgba(52, 199, 89, 0.3)" },
|
||||
"blue_switch": { "--bg-window": "246, 249, 253", "--bg-nav": "235, 244, 255", "--bg-panel": "255, 255, 255", "--bg-hover": "rgba(0,122,255,0.05)", "--text-primary": "29, 29, 31", "--text-secondary": "134, 134, 139", "--border-color":"220, 230, 250", "--accent-color":"0, 122, 255", "--accent-alpha":"rgba(0, 122, 255, 0.1)", "--accent-shadow":"rgba(0, 122, 255, 0.2)", "--accent-hover":"rgba(0, 122, 255, 0.3)" }
|
||||
};
|
||||
|
||||
const cols = {
|
||||
log: [{key: 'time_str', label: 'col_date', flex: 1.5}, {key: 'level', label: 'col_type', flex: 0.8}, {key: 'action', label: 'col_func', flex: 1.2, isI18n: true}, {key: 'source', label: 'col_path', flex: 3}],
|
||||
process: [{key: 'name', label: 'col_name', flex: 1.5}, {key: 'pid', label: 'col_pid', flex: 0.5}, {key: 'path', label: 'col_path', flex: 3}],
|
||||
startup: [{key: 'type', label: 'col_type', flex: 0.5, isI18n: true}, {key: 'name', label: 'col_name', flex: 1.5}, {key: 'status', label: 'col_status', flex: 0.5, isI18n: true}, {key: 'path', label: 'col_path', flex: 3}],
|
||||
virus: [{key: 'label', label: 'col_type', flex: 1, isI18n: true}, {key: 'path', label: 'col_path', flex: 3}],
|
||||
pathOnly: [{key: 'path', label: 'col_path', flex: 1}],
|
||||
junk: [{key: 'path', label: 'col_path', flex: 3}, {key: 'sizeStr', label: 'col_size', flex: 1}],
|
||||
popup: [{key: 'exe', label: 'col_prog', flex: 1}, {key: 'class', label: 'col_class', flex: 1}, {key: 'title', label: 'col_title', flex: 2}],
|
||||
repair: [{key: 'display', label: 'col_repair', flex: 1, isI18n: true}, {key: 'path', label: 'col_path', flex: 3}]
|
||||
};
|
||||
|
||||
const buildCustomSelectElement = (select) => {
|
||||
select.style.display = 'none';
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'custom-select-wrapper';
|
||||
|
||||
const selectedOpt = select.options[select.selectedIndex];
|
||||
const triggerKey = selectedOpt ? selectedOpt.getAttribute('data-i18n') : '';
|
||||
const triggerTextContent = selectedOpt ? getMsg(triggerKey || selectedOpt.textContent) : '';
|
||||
|
||||
const triggerHTML = `
|
||||
<div class="custom-select-trigger">
|
||||
<span class="custom-select-text" ${triggerKey ? `data-i18n="${triggerKey}"` : ''}>${triggerTextContent}</span>
|
||||
<div class="custom-select-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></div>
|
||||
</div>`;
|
||||
wrapper.innerHTML = triggerHTML;
|
||||
|
||||
const optionsContainer = document.createElement('ul');
|
||||
optionsContainer.className = 'custom-select-options';
|
||||
|
||||
const phantom = document.createElement('div');
|
||||
phantom.className = 'custom-select-phantom';
|
||||
phantom.setAttribute('aria-hidden', 'true');
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
if (option.disabled) return;
|
||||
const optKey = option.getAttribute('data-i18n');
|
||||
const optText = optKey ? getMsg(optKey) : option.textContent;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = `custom-select-option ${option.selected ? 'selected' : ''}`;
|
||||
li.textContent = optText;
|
||||
li.dataset.value = option.value;
|
||||
if (optKey) li.setAttribute('data-i18n', optKey);
|
||||
|
||||
li.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
select.value = option.value;
|
||||
const triggerText = wrapper.querySelector('.custom-select-text');
|
||||
triggerText.textContent = li.textContent;
|
||||
if (optKey) triggerText.setAttribute('data-i18n', optKey);
|
||||
|
||||
wrapper.classList.remove('open');
|
||||
optionsContainer.querySelectorAll('.custom-select-option').forEach(el => el.classList.remove('selected'));
|
||||
li.classList.add('selected');
|
||||
select.dispatchEvent(new Event('change'));
|
||||
});
|
||||
optionsContainer.appendChild(li);
|
||||
|
||||
const phantomSpan = document.createElement('span');
|
||||
phantomSpan.textContent = optText;
|
||||
if (optKey) phantomSpan.setAttribute('data-i18n', optKey);
|
||||
phantom.appendChild(phantomSpan);
|
||||
});
|
||||
|
||||
wrapper.appendChild(phantom);
|
||||
wrapper.appendChild(optionsContainer);
|
||||
select.parentNode.insertBefore(wrapper, select.nextSibling);
|
||||
|
||||
const trigger = wrapper.querySelector('.custom-select-trigger');
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = wrapper.classList.contains('open');
|
||||
document.querySelectorAll('.custom-select-wrapper').forEach(w => w.classList.remove('open'));
|
||||
if (!isOpen) wrapper.classList.add('open');
|
||||
});
|
||||
};
|
||||
|
||||
const updateCustomSelectUI = (selectId, val) => {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
select.value = val;
|
||||
const wrapper = select.nextElementSibling;
|
||||
if (wrapper && wrapper.classList.contains('custom-select-wrapper')) {
|
||||
const triggerText = wrapper.querySelector('.custom-select-text');
|
||||
const selectedOpt = select.options[select.selectedIndex];
|
||||
if (selectedOpt) {
|
||||
const optKey = selectedOpt.getAttribute('data-i18n');
|
||||
triggerText.textContent = optKey ? getMsg(optKey) : selectedOpt.textContent;
|
||||
if (optKey) triggerText.setAttribute('data-i18n', optKey);
|
||||
}
|
||||
wrapper.querySelectorAll('.custom-select-option').forEach(el => {
|
||||
el.classList.toggle('selected', el.dataset.value === val);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const translateText = (lang) => {
|
||||
appState.lang = lang;
|
||||
document.querySelectorAll("[data-i18n]").forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (key) el.textContent = getMsg(key);
|
||||
});
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
if (key) el.setAttribute('placeholder', getMsg(key));
|
||||
});
|
||||
document.querySelectorAll('.manage-widget, .virus-widget').forEach(widget => {
|
||||
if (widget.buildHeader) widget.buildHeader();
|
||||
if (widget.renderData) widget.renderData();
|
||||
});
|
||||
document.documentElement.lang = langMap[lang] || "en";
|
||||
|
||||
const progressText = document.getElementById('progress_text');
|
||||
if (progressText && progressText.dataset.dynamicMsg) {
|
||||
try {
|
||||
const msgs = JSON.parse(progressText.dataset.dynamicMsg);
|
||||
if (msgs[lang]) progressText.textContent = msgs[lang];
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
const applyTheme = (theme) => {
|
||||
appState.theme = theme;
|
||||
const actualTheme = theme === "system_switch"
|
||||
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? "black_switch" : "white_switch")
|
||||
: theme;
|
||||
|
||||
const themeVars = themes[actualTheme];
|
||||
if (themeVars) {
|
||||
for (const [key, value] of Object.entries(themeVars)) {
|
||||
document.documentElement.style.setProperty(key, value.startsWith("rgba") ? value : `rgb(${value})`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (appState.theme === "system_switch") applyTheme("system_switch");
|
||||
});
|
||||
|
||||
const renderDataGrid = (containerSelector, dataList, columns, valKey, defaultChecked = false) => {
|
||||
const widget = document.querySelector(`${containerSelector} .manage-widget`) || document.querySelector(`${containerSelector} .virus-widget`);
|
||||
const listUl = document.querySelector(`${containerSelector} .manage-list`) || document.querySelector(`${containerSelector} .virus-list`);
|
||||
const searchInput = document.querySelector(`${containerSelector} .search-box input`);
|
||||
if (!widget || !listUl) return;
|
||||
|
||||
const isInit = !widget.gridState;
|
||||
if (isInit) {
|
||||
const containerWidth = widget.clientWidth || 800;
|
||||
const availableWidth = Math.max(containerWidth - 60 - (columns.length * 16), 400);
|
||||
const totalFlex = columns.reduce((sum, c) => sum + (c.flex || 1), 0);
|
||||
|
||||
widget.gridState = {
|
||||
sortKey: columns[0]?.key || '', sortAsc: true, filterText: "",
|
||||
colWidths: columns.map(c => Math.max(((c.flex || 1) / totalFlex) * availableWidth, 60)),
|
||||
checkedSet: new Set()
|
||||
};
|
||||
|
||||
widget.gridState.colWidths.forEach((w, idx) => widget.style.setProperty(`--col-${idx}`, `${w}px`));
|
||||
const header = document.createElement('div');
|
||||
header.className = 'manage-list-header';
|
||||
widget.insertBefore(header, widget.firstChild);
|
||||
|
||||
widget.buildHeader = () => {
|
||||
const headerColsHtml = columns.map((col, idx) => `
|
||||
<div class="col-header" data-key="${col.key}" data-idx="${idx}" style="width: var(--col-${idx}); flex: 0 0 auto;">
|
||||
<span class="header-text" data-i18n="${col.label}">${getMsg(col.label)}</span>
|
||||
<span class="sort-icon">${widget.gridState.sortKey === col.key ? (widget.gridState.sortAsc ? '▲' : '▼') : ''}</span>
|
||||
<div class="col-resizer"></div>
|
||||
</div>`).join('');
|
||||
|
||||
header.innerHTML = `<input type="checkbox" class="select-all-cb" title="${getMsg('btn_select_all')}"><div class="header-cols-container">${headerColsHtml}</div>`;
|
||||
|
||||
header.querySelectorAll('.col-header').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const key = el.dataset.key;
|
||||
widget.gridState.sortAsc = widget.gridState.sortKey === key ? !widget.gridState.sortAsc : true;
|
||||
widget.gridState.sortKey = key;
|
||||
widget.buildHeader();
|
||||
widget.renderData(widget.gridState.filterText, false);
|
||||
});
|
||||
});
|
||||
|
||||
header.querySelectorAll('.col-resizer').forEach(resizer => {
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = parseInt(resizer.parentElement.dataset.idx);
|
||||
const startX = e.clientX;
|
||||
const startWidth = widget.gridState.colWidths[idx];
|
||||
|
||||
const onMouseMove = (me) => {
|
||||
const newWidth = Math.max(startWidth + (me.clientX - startX), 40);
|
||||
widget.gridState.colWidths[idx] = newWidth;
|
||||
widget.style.setProperty(`--col-${idx}`, `${newWidth}px`);
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
document.body.classList.remove('resizing-active');
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
document.body.classList.add('resizing-active');
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
resizer.addEventListener('click', (e) => e.stopPropagation());
|
||||
});
|
||||
|
||||
const selectAllCb = header.querySelector('.select-all-cb');
|
||||
if (selectAllCb) {
|
||||
selectAllCb.addEventListener('change', (e) => {
|
||||
const isChecked = e.target.checked;
|
||||
listUl.querySelectorAll('.manage-list-item input[type="checkbox"]').forEach(cb => cb.checked = isChecked);
|
||||
(widget.gridState.lastDisplayData || []).forEach(item => {
|
||||
const valStr = String(item[valKey]);
|
||||
isChecked ? widget.gridState.checkedSet.add(valStr) : widget.gridState.checkedSet.delete(valStr);
|
||||
if (containerSelector === '#scan_window') appState.virusMap.set(valStr, isChecked);
|
||||
});
|
||||
if (containerSelector === '#scan_window') checkVirusListEmpty();
|
||||
});
|
||||
}
|
||||
};
|
||||
widget.buildHeader();
|
||||
|
||||
listUl.addEventListener('click', (e) => {
|
||||
const li = e.target.closest('.manage-list-item');
|
||||
if (!li) return;
|
||||
const cb = li.querySelector('input[type="checkbox"]');
|
||||
if (e.target.tagName !== 'INPUT') cb.checked = !cb.checked;
|
||||
|
||||
cb.checked ? widget.gridState.checkedSet.add(cb.value) : widget.gridState.checkedSet.delete(cb.value);
|
||||
if (containerSelector === '#scan_window') {
|
||||
appState.virusMap.set(cb.value, cb.checked);
|
||||
checkVirusListEmpty();
|
||||
}
|
||||
if (widget.updateSelectAllState) widget.updateSelectAllState();
|
||||
});
|
||||
|
||||
if (searchInput) searchInput.addEventListener('input', (e) => widget.renderData(e.target.value, false));
|
||||
}
|
||||
|
||||
const state = widget.gridState;
|
||||
widget.renderData = (filterText = state.filterText, resetDataCheck = false) => {
|
||||
state.filterText = filterText;
|
||||
if (resetDataCheck) {
|
||||
state.checkedSet.clear();
|
||||
if (defaultChecked) dataList.forEach(item => state.checkedSet.add(String(item[valKey])));
|
||||
} else {
|
||||
const validKeys = new Set(dataList.map(item => String(item[valKey])));
|
||||
for (const val of state.checkedSet) if (!validKeys.has(val)) state.checkedSet.delete(val);
|
||||
if (state.checkedSet.size === 0 && defaultChecked && dataList.length > 0) {
|
||||
if (!listUl.querySelector('input[type="checkbox"]:checked')) {
|
||||
dataList.forEach(item => state.checkedSet.add(String(item[valKey])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const header = widget.querySelector('.manage-list-header');
|
||||
const selectAllCb = header ? header.querySelector('.select-all-cb') : null;
|
||||
if (selectAllCb) selectAllCb.disabled = dataList.length === 0;
|
||||
|
||||
const ft = filterText.toLowerCase();
|
||||
let displayData = dataList.filter(item => !filterText || columns.some(col => String(item[col.key] || '').toLowerCase().includes(ft)));
|
||||
|
||||
displayData.sort((a, b) => {
|
||||
const valA = a[state.sortKey] ?? '', valB = b[state.sortKey] ?? '';
|
||||
if (state.sortKey === 'sizeStr' && 'size' in a && 'size' in b) return state.sortAsc ? a.size - b.size : b.size - a.size;
|
||||
const numA = parseFloat(valA), numB = parseFloat(valB);
|
||||
if (!isNaN(numA) && !isNaN(numB) && String(valA).trim() !== '' && String(valB).trim() !== '') return state.sortAsc ? numA - numB : numB - numA;
|
||||
return state.sortAsc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA));
|
||||
});
|
||||
|
||||
widget.gridState.lastDisplayData = displayData;
|
||||
widget.updateSelectAllState = () => {
|
||||
if (!selectAllCb) return;
|
||||
const cData = widget.gridState.lastDisplayData || [];
|
||||
if (cData.length === 0) { selectAllCb.checked = false; selectAllCb.indeterminate = false; return; }
|
||||
const checkedCount = cData.filter(item => containerSelector === '#scan_window' ? appState.virusMap.get(String(item[valKey])) : widget.gridState.checkedSet.has(String(item[valKey]))).length;
|
||||
selectAllCb.checked = (checkedCount > 0 && checkedCount === cData.length);
|
||||
selectAllCb.indeterminate = (checkedCount > 0 && checkedCount < cData.length);
|
||||
};
|
||||
|
||||
const MAX_ITEMS = 2000;
|
||||
const limitData = displayData.slice(0, MAX_ITEMS);
|
||||
|
||||
let htmlContent = limitData.map(item => {
|
||||
const itemValStr = String(item[valKey]);
|
||||
let isChecked = containerSelector === '#scan_window' ? (appState.virusMap.get(itemValStr) ?? true) : state.checkedSet.has(itemValStr);
|
||||
if (containerSelector === '#scan_window') appState.virusMap.set(itemValStr, isChecked);
|
||||
|
||||
const rowColsHtml = columns.map((col, idx) => {
|
||||
const rawVal = String(item[col.key] || '');
|
||||
let valStr = col.isI18n ? getMsg(rawVal) : rawVal;
|
||||
if (rawVal === '未知路徑' || rawVal === 'path_unknown') valStr = getMsg('path_unknown');
|
||||
return `<div class="row-col" style="width: var(--col-${idx}); flex: 0 0 auto;" title="${escapeHtml(valStr)}">${escapeHtml(valStr)}</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<li class="manage-list-item"><input type="checkbox" value="${escapeHtml(itemValStr)}" ${isChecked ? 'checked' : ''} data-path="${escapeHtml(item.path || '')}"><div class="manage-list-item-content">${rowColsHtml}</div></li>`;
|
||||
}).join('');
|
||||
|
||||
if (displayData.length > MAX_ITEMS) {
|
||||
htmlContent += `<li class="manage-list-item" style="pointer-events: none; justify-content: center;"><div style="color: var(--text-secondary); padding: 10px;">${escapeHtml(formatMsg("msg_too_many", MAX_ITEMS))}</div></li>`;
|
||||
}
|
||||
|
||||
const scrollContainer = listUl.parentElement;
|
||||
const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
|
||||
listUl.innerHTML = htmlContent;
|
||||
if (scrollContainer) scrollContainer.scrollTop = scrollTop;
|
||||
widget.updateSelectAllState();
|
||||
};
|
||||
widget.renderData(state.filterText, isInit);
|
||||
};
|
||||
|
||||
const getCheckedValues = (selector) => Array.from(document.querySelectorAll(`${selector} .manage-list-item input[type="checkbox"]:checked`)).map(cb => cb.value);
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'], i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const rebuildCustomSelect = (selectId) => {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
let sibling = select.nextElementSibling;
|
||||
while (sibling && sibling.classList.contains('custom-select-wrapper')) {
|
||||
const toRemove = sibling;
|
||||
sibling = sibling.nextElementSibling;
|
||||
toRemove.remove();
|
||||
}
|
||||
buildCustomSelectElement(select);
|
||||
};
|
||||
|
||||
const changeScanSelectMode = (mode) => {
|
||||
const select = document.getElementById('scan_method_select');
|
||||
if (!select) return;
|
||||
select.innerHTML = `<option value="none" selected disabled data-i18n="btn_select">${getMsg('btn_select')}</option>`;
|
||||
if (mode === 'scan') {
|
||||
select.innerHTML += `
|
||||
<option value="smart" data-i18n="scan_smart">${getMsg('scan_smart')}</option>
|
||||
<option value="file" data-i18n="scan_file">${getMsg('scan_file')}</option>
|
||||
<option value="path" data-i18n="scan_path">${getMsg('scan_path')}</option>
|
||||
<option value="full" data-i18n="scan_full">${getMsg('scan_full')}</option>`;
|
||||
} else if (mode === 'action') {
|
||||
select.innerHTML += `
|
||||
<option value="delete" data-i18n="act_del">${getMsg('act_del')}</option>
|
||||
<option value="ignore" data-i18n="act_ignore">${getMsg('act_ignore')}</option>
|
||||
<option value="quarantine" data-i18n="act_quarantine">${getMsg('act_quarantine')}</option>
|
||||
<option value="whitelist" data-i18n="act_white">${getMsg('act_white')}</option>`;
|
||||
}
|
||||
rebuildCustomSelect('scan_method_select');
|
||||
};
|
||||
|
||||
const checkVirusListEmpty = () => {
|
||||
updateCustomSelectUI('scan_method_select', 'none');
|
||||
if (appState.virusResults.length === 0) {
|
||||
appState.virusMap.clear();
|
||||
changeScanSelectMode('scan');
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) {
|
||||
title.setAttribute('data-i18n', 'scan_virus');
|
||||
title.textContent = getMsg('scan_virus');
|
||||
}
|
||||
if (text) {
|
||||
text.setAttribute('data-i18n', 'scan_virus_desc');
|
||||
text.removeAttribute('data-dynamic-msg');
|
||||
text.textContent = getMsg('scan_virus_desc');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const triggerScan = (method) => {
|
||||
if (!window.pywebview) return;
|
||||
appState.scanning = true;
|
||||
appState.virusMap.clear();
|
||||
appState.virusResults = [];
|
||||
renderDataGrid('#scan_window', appState.virusResults, cols.virus, 'path', true);
|
||||
changeScanSelectMode('scan');
|
||||
document.getElementById('scan_method_select').classList.add('hidden');
|
||||
const stopBtn = document.getElementById('stop_btn');
|
||||
stopBtn.classList.remove('hidden');
|
||||
stopBtn.disabled = false;
|
||||
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) { title.setAttribute('data-i18n', 'status_scanning'); title.textContent = getMsg('status_scanning'); }
|
||||
if (text) { text.setAttribute('data-i18n', 'msg_init'); text.removeAttribute('data-dynamic-msg'); text.textContent = getMsg('msg_init'); }
|
||||
window.pywebview.api.trigger_scan(method);
|
||||
};
|
||||
|
||||
const handleVirusActions = (action) => {
|
||||
if (!window.pywebview) return;
|
||||
const paths = Array.from(appState.virusMap.entries()).filter(([_, checked]) => checked).map(([p]) => p);
|
||||
if (paths.length === 0) return updateCustomSelectUI('scan_method_select', 'none');
|
||||
|
||||
const wrapper = document.getElementById('scan_method_select').nextElementSibling;
|
||||
if (wrapper) wrapper.style.pointerEvents = 'none';
|
||||
|
||||
const finalize = () => {
|
||||
if (wrapper) wrapper.style.pointerEvents = '';
|
||||
checkVirusListEmpty();
|
||||
};
|
||||
|
||||
const removeItems = (removedPaths) => {
|
||||
const removedSet = new Set(removedPaths);
|
||||
removedPaths.forEach(p => appState.virusMap.delete(p));
|
||||
appState.virusResults = appState.virusResults.filter(r => !removedSet.has(r.path));
|
||||
renderDataGrid('#scan_window', appState.virusResults, cols.virus, 'path', false);
|
||||
};
|
||||
|
||||
if (action === 'delete') {
|
||||
window.pywebview.api.solve_scan(paths).then((deleted) => { removeItems(deleted); finalize(); });
|
||||
} else if (action === 'ignore') {
|
||||
window.pywebview.api.remove_virus_result(paths).then(() => { removeItems(paths); finalize(); });
|
||||
} else if (action === 'quarantine' || action === 'whitelist') {
|
||||
const listKey = action === 'quarantine' ? 'quarantine' : 'white_list';
|
||||
window.pywebview.api.manage_named_list(listKey, paths, 'add').then(() => {
|
||||
window.pywebview.api.remove_virus_result(paths).then(() => { removeItems(paths); finalize(); });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProcs = () => {
|
||||
if (!appState.taskmgrActive || !window.pywebview) return;
|
||||
const widget = document.querySelector('#taskmgr_window .manage-widget');
|
||||
const blockFetch = widget && (widget.gridState?.checkedSet.size > 0 || widget.matches(':hover')) || document.querySelector('.custom-context-menu.show') || document.body.classList.contains('resizing-active');
|
||||
|
||||
if (!blockFetch) {
|
||||
window.pywebview.api.get_process_list().then(procs => {
|
||||
const mapped = procs.map(p => ({ name: p.name, pid: p.pid, path: (p.path && p.path !== "None" && p.path !== "Unknown") ? p.path : "path_unknown" }));
|
||||
renderDataGrid('#taskmgr_window', mapped, cols.process, 'pid', false);
|
||||
if (appState.taskmgrActive) appState.taskmgrTimer = setTimeout(fetchProcs, 2000);
|
||||
}).catch(() => { if (appState.taskmgrActive) appState.taskmgrTimer = setTimeout(fetchProcs, 2000); });
|
||||
} else {
|
||||
if (appState.taskmgrActive) appState.taskmgrTimer = setTimeout(fetchProcs, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStartup = () => {
|
||||
if (!window.pywebview) return;
|
||||
const listUl = document.querySelector('#startup_window .manage-list');
|
||||
if (listUl && !listUl.children.length) {
|
||||
listUl.innerHTML = `<li class="manage-list-item" style="pointer-events: none; justify-content: center; height: 100%; border: none;"><div style="color: var(--text-secondary); padding: 40px; font-size: 15px;">${getMsg('msg_init')}</div></li>`;
|
||||
}
|
||||
window.pywebview.api.get_startup_list().then(items => {
|
||||
const mapped = items.map(item => ({ id: item.id, type: item.type, name: item.name, status: item.status === 'enabled' ? 'status_enabled' : 'status_disabled', path: item.path || 'path_unknown' }));
|
||||
renderDataGrid('#startup_window', mapped, cols.startup, 'id', false);
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelector('#startup_window .primary-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#startup_window');
|
||||
if (checked.length > 0 && window.pywebview) {
|
||||
const widget = document.querySelector('#startup_window .manage-widget');
|
||||
if (widget && widget.gridState && widget.gridState.lastDisplayData) {
|
||||
widget.gridState.lastDisplayData.forEach(item => {
|
||||
if (checked.includes(item.id)) item.status = 'status_enabled';
|
||||
});
|
||||
widget.renderData();
|
||||
}
|
||||
window.pywebview.api.manage_startup(checked, 'enable').then(fetchStartup);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#startup_window .danger-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#startup_window');
|
||||
if (checked.length > 0 && window.pywebview) {
|
||||
const widget = document.querySelector('#startup_window .manage-widget');
|
||||
if (widget && widget.gridState && widget.gridState.lastDisplayData) {
|
||||
widget.gridState.lastDisplayData.forEach(item => {
|
||||
if (checked.includes(item.id)) item.status = 'status_disabled';
|
||||
});
|
||||
widget.renderData();
|
||||
}
|
||||
window.pywebview.api.manage_startup(checked, 'disable').then(fetchStartup);
|
||||
}
|
||||
});
|
||||
|
||||
const refreshConfigLists = () => {
|
||||
if (!window.pywebview) return;
|
||||
window.pywebview.api.get_config().then(cfg => {
|
||||
renderDataGrid('#whitelist_window', (cfg.white_list || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
|
||||
renderDataGrid('#quarantine_window', (cfg.quarantine || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
|
||||
renderDataGrid('#custom_protect_window', (cfg.custom_rule || []).map(i => ({path: i.file || i})), cols.pathOnly, 'path');
|
||||
const popupRules = (cfg.block_list || []).map(item => ({ exe: item.exe || '*', class: item.class || '*', title: item.title || '*', value: item.exe || item.title }));
|
||||
renderDataGrid('#popup_window', popupRules, cols.popup, 'value');
|
||||
});
|
||||
};
|
||||
|
||||
const switchPage = (targetId) => {
|
||||
const oldActive = document.querySelector('.page.active');
|
||||
if (oldActive && oldActive.id !== targetId) {
|
||||
if (oldActive.id === 'junk_window') {
|
||||
appState.junkList = [];
|
||||
renderDataGrid('#junk_window', [], cols.junk, 'path', true);
|
||||
const input = document.querySelector('#junk_window .search-box input');
|
||||
if (input) input.value = '';
|
||||
} else if (oldActive.id === 'repair_window') {
|
||||
renderDataGrid('#repair_window', [], cols.repair, 'id', true);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.page, .nav-btn').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(targetId)?.classList.add('active');
|
||||
document.querySelector(`aside .nav-btn[data-target="${targetId}"]`)?.classList.add('active');
|
||||
|
||||
appState.taskmgrActive = targetId === 'taskmgr_window';
|
||||
if (appState.taskmgrActive) fetchProcs();
|
||||
else if (appState.taskmgrTimer) { clearTimeout(appState.taskmgrTimer); appState.taskmgrTimer = null; }
|
||||
|
||||
if (targetId === 'startup_window') fetchStartup();
|
||||
|
||||
if (['whitelist_window', 'quarantine_window', 'popup_window', 'custom_protect_window'].includes(targetId)) refreshConfigLists();
|
||||
|
||||
if (targetId === 'log_export_window' && window.pywebview) {
|
||||
window.pywebview.api.get_logs().then(logs => {
|
||||
const logList = logs.reverse().map(log => ({ time_str: `[${log.time_str}]`, level: log.level, action: log.action, source: log.source || '-', id: log.id }));
|
||||
renderDataGrid('#log_export_window', logList, cols.log, 'id', true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addVirusResult = (label, path) => {
|
||||
if (!appState.virusMap.has(path)) {
|
||||
appState.virusResults.push({ label, path });
|
||||
appState.virusMap.set(path, true);
|
||||
const widget = document.querySelector('#scan_window .virus-widget');
|
||||
if (widget && widget.renderData) {
|
||||
if (window.virusRenderTimeout) clearTimeout(window.virusRenderTimeout);
|
||||
window.virusRenderTimeout = setTimeout(() => widget.renderData(widget.gridState.filterText, false), 50);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.updateScanProgress = (path) => {
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) { title.setAttribute('data-i18n', 'status_scanning'); title.textContent = getMsg('status_scanning'); }
|
||||
if (text) { text.removeAttribute('data-i18n'); text.removeAttribute('data-dynamic-msg'); text.textContent = path; }
|
||||
};
|
||||
|
||||
window.updateDeleteProgress = (path) => {
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) { title.setAttribute('data-i18n', 'status_deleting'); title.textContent = getMsg('status_deleting'); }
|
||||
if (text) { text.removeAttribute('data-i18n'); text.removeAttribute('data-dynamic-msg'); text.textContent = path; }
|
||||
};
|
||||
|
||||
window.finishScan = (msgData, count) => {
|
||||
appState.scanning = false;
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) { title.setAttribute('data-i18n', 'scan_virus'); title.textContent = getMsg('scan_virus'); }
|
||||
if (text) {
|
||||
if (typeof msgData === 'object' && msgData !== null) {
|
||||
text.removeAttribute('data-i18n');
|
||||
text.dataset.dynamicMsg = JSON.stringify(msgData);
|
||||
text.textContent = msgData[appState.lang] || msgData["english_switch"];
|
||||
} else {
|
||||
text.removeAttribute('data-dynamic-msg');
|
||||
text.setAttribute('data-i18n', msgData);
|
||||
text.textContent = getMsg(msgData);
|
||||
}
|
||||
}
|
||||
document.getElementById('stop_btn')?.classList.add('hidden');
|
||||
document.getElementById('scan_method_select')?.classList.remove('hidden');
|
||||
changeScanSelectMode(count > 0 ? 'action' : 'scan');
|
||||
updateCustomSelectUI('scan_method_select', 'none');
|
||||
};
|
||||
|
||||
window.updateLogs = (entry) => {
|
||||
const logWidget = document.querySelector('.log-text');
|
||||
if (logWidget) {
|
||||
let parts = [`[${entry.time_str}]`, entry.level, getMsg(entry.action || '')];
|
||||
if (entry.source) parts.push(`Src: ${entry.source}`);
|
||||
if (entry.target) parts.push(`Tgt: ${entry.target}`);
|
||||
if (entry.code) parts.push(`Code: ${entry.code}`);
|
||||
if (entry.pid) parts.push(`PID: ${entry.pid}`);
|
||||
if (entry.hash) parts.push(`Hash: ${entry.hash}`);
|
||||
if (entry.detail) parts.push(`Detail: ${entry.detail}`);
|
||||
if (entry.operate !== null) parts.push(`Op: ${entry.operate}`);
|
||||
parts.push(`Success: ${entry.success}`);
|
||||
let val = logWidget.value + parts.join(' | ') + '\n';
|
||||
if (val.length > 20000) val = val.substring(val.indexOf('\n', val.length - 15000) + 1);
|
||||
logWidget.value = val;
|
||||
logWidget.scrollTop = logWidget.scrollHeight;
|
||||
}
|
||||
|
||||
if (document.getElementById('log_export_window')?.classList.contains('active') && window.pywebview) {
|
||||
window.pywebview.api.get_logs().then(logs => {
|
||||
const logList = logs.reverse().map(log => ({ time_str: `[${log.time_str}]`, level: log.level, action: log.action, source: log.source || '-', id: log.id }));
|
||||
renderDataGrid('#log_export_window', logList, cols.log, 'id', true);
|
||||
});
|
||||
}
|
||||
|
||||
if ((entry.action === 'System' && entry.detail === 'Engine Initialization Complete') || (entry.level === 'WARN' && entry.action === 'init_engine_thread')) {
|
||||
const overlay = document.getElementById('loading_overlay');
|
||||
if (overlay && !overlay.classList.contains('fade-out')) {
|
||||
overlay.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
document.querySelector('.app-container')?.classList.add('fade-in');
|
||||
if (window.onEngineReady) window.onEngineReady();
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.triggerContextScan = (targets) => {
|
||||
if (appState.scanning) return;
|
||||
switchPage('scan_window');
|
||||
if (window.pywebview) {
|
||||
appState.scanning = true;
|
||||
appState.virusMap.clear();
|
||||
appState.virusResults = [];
|
||||
renderDataGrid('#scan_window', appState.virusResults, cols.virus, 'path', true);
|
||||
document.getElementById('scan_method_select')?.classList.add('hidden');
|
||||
const stopBtn = document.getElementById('stop_btn');
|
||||
if (stopBtn) { stopBtn.classList.remove('hidden'); stopBtn.disabled = false; }
|
||||
changeScanSelectMode('scan');
|
||||
|
||||
const title = document.querySelector('#scan_window .section-title');
|
||||
const text = document.getElementById('progress_text');
|
||||
if (title) { title.setAttribute('data-i18n', 'status_scanning'); title.textContent = getMsg('status_scanning'); }
|
||||
if (text) { text.setAttribute('data-i18n', 'msg_init'); text.removeAttribute('data-dynamic-msg'); text.textContent = getMsg('msg_init'); }
|
||||
|
||||
const paths = Array.isArray(targets) ? targets : [targets];
|
||||
window.pywebview.api.start_scan(paths);
|
||||
}
|
||||
};
|
||||
|
||||
window.revertSwitch = () => {};
|
||||
|
||||
document.querySelectorAll('[data-target]').forEach(btn => btn.addEventListener('click', (e) => switchPage(e.currentTarget.getAttribute('data-target'))));
|
||||
document.getElementById('quick_scan_btn')?.addEventListener('click', () => { if (!appState.scanning) { switchPage('scan_window'); triggerScan('smart'); }});
|
||||
document.getElementById('stop_btn')?.addEventListener('click', (e) => { if (window.pywebview) window.pywebview.api.stop_scan(); e.target.disabled = true; });
|
||||
document.getElementById('theme_select')?.addEventListener('change', (e) => { applyTheme(e.target.value); window.pywebview?.api.update_config('theme', e.target.value); });
|
||||
document.getElementById('lang_select')?.addEventListener('change', (e) => { translateText(e.target.value); window.pywebview?.api.update_config('language', e.target.value); });
|
||||
|
||||
document.getElementById('scan_method_select')?.addEventListener('change', (e) => {
|
||||
const val = e.target.value;
|
||||
if (val === 'none' || appState.scanning) return;
|
||||
if (['smart', 'file', 'path', 'full'].includes(val)) triggerScan(val);
|
||||
else if (['delete', 'ignore', 'quarantine', 'whitelist'].includes(val)) handleVirusActions(val);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.toggle-switch input').forEach((toggle, index) => {
|
||||
toggle.addEventListener('change', async (e) => {
|
||||
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
|
||||
const key = switchMap[index];
|
||||
if (window.pywebview && key) {
|
||||
toggle.disabled = true;
|
||||
try {
|
||||
const result = await window.pywebview.api.update_config(key, e.target.checked);
|
||||
if (result !== undefined && result !== null) toggle.checked = result;
|
||||
} catch { toggle.checked = !e.target.checked; }
|
||||
finally { toggle.disabled = false; }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#junk_window .primary-btn')?.addEventListener('click', (e) => {
|
||||
if (!window.pywebview) return;
|
||||
e.target.setAttribute('data-i18n', 'msg_init'); e.target.textContent = getMsg('msg_init');
|
||||
const widget = document.querySelector('#junk_window .manage-widget');
|
||||
if (widget?.gridState) widget.gridState.checkedSet.clear();
|
||||
|
||||
window.pywebview.api.scan_system_junk().then(list => {
|
||||
appState.junkList = list.map(item => ({ path: item.path, sizeStr: formatSize(item.size), size: item.size }));
|
||||
renderDataGrid('#junk_window', appState.junkList, cols.junk, 'path', true);
|
||||
e.target.setAttribute('data-i18n', 'btn_scan'); e.target.textContent = getMsg('btn_scan');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#junk_window .danger-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#junk_window');
|
||||
if (checked.length === 0 || !window.pywebview) return;
|
||||
window.pywebview.api.clean_system_junk(checked).then(size => {
|
||||
window.pywebview.api.show_alert(getMsg("title_prompt"), `${getMsg("msg_cleaned")}${formatSize(size)}`, "info");
|
||||
appState.junkList = appState.junkList.filter(item => !checked.includes(item.path));
|
||||
renderDataGrid('#junk_window', appState.junkList, cols.junk, 'path', true);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#repair_window .primary-btn')?.addEventListener('click', () => {
|
||||
if (!window.pywebview) return;
|
||||
window.pywebview.api.scan_system_repair().then(list => renderDataGrid('#repair_window', list, cols.repair, 'id', true));
|
||||
});
|
||||
|
||||
document.querySelector('#repair_window .danger-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#repair_window');
|
||||
if (checked.length === 0 || !window.pywebview) return;
|
||||
window.pywebview.api.execute_system_repair(checked).then(() => {
|
||||
window.pywebview.api.show_alert(getMsg("title_prompt"), getMsg("msg_repaired"), "info");
|
||||
renderDataGrid('#repair_window', [], cols.repair, 'id', true);
|
||||
});
|
||||
});
|
||||
|
||||
['whitelist_window', 'quarantine_window', 'popup_window', 'custom_protect_window'].forEach(id => {
|
||||
const listKeyMap = {
|
||||
'popup_window': 'block_list',
|
||||
'custom_protect_window': 'custom_rule',
|
||||
'whitelist_window': 'white_list',
|
||||
'quarantine_window': 'quarantine'
|
||||
};
|
||||
const listKey = listKeyMap[id];
|
||||
|
||||
document.querySelector(`#${id} .modern-btn:not(.primary-btn)`)?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues(`#${id}`);
|
||||
if (checked.length > 0 && window.pywebview) window.pywebview.api.remove_list_items(listKey, checked).then(refreshConfigLists);
|
||||
});
|
||||
|
||||
if (id !== 'popup_window') {
|
||||
document.querySelector(`#${id} .primary-btn`)?.addEventListener('click', () => {
|
||||
window.pywebview?.api.select_files().then(files => {
|
||||
if (files?.length > 0) window.pywebview.api.manage_named_list(listKey, files, 'add').then(refreshConfigLists);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#popup_window .primary-btn')?.addEventListener('click', (e) => {
|
||||
if (!window.pywebview) return;
|
||||
e.target.setAttribute('data-i18n', 'msg_click_target'); e.target.textContent = getMsg('msg_click_target'); e.target.disabled = true;
|
||||
window.pywebview.api.capture_popup_window().then(rule => {
|
||||
e.target.setAttribute('data-i18n', 'btn_add'); e.target.textContent = getMsg('btn_add'); e.target.disabled = false;
|
||||
if (rule) window.pywebview.api.add_popup_rule(rule).then(ok => ok && refreshConfigLists());
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#taskmgr_window .primary-btn')?.addEventListener('click', () => {
|
||||
if (!window.pywebview) return;
|
||||
document.querySelectorAll('#taskmgr_window .manage-list-item input[type="checkbox"]:checked').forEach(cb => {
|
||||
if (cb.dataset.path && cb.dataset.path !== "path_unknown") window.pywebview.api.open_file_location(cb.dataset.path);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#taskmgr_window .danger-btn')?.addEventListener('click', () => {
|
||||
const cbs = document.querySelectorAll('#taskmgr_window .manage-list-item input[type="checkbox"]:checked');
|
||||
if (cbs.length > 0 && window.pywebview) {
|
||||
window.pywebview.api.show_confirm(getMsg("msg_end_proc_title"), getMsg("msg_end_proc_confirm")).then(res => {
|
||||
if (res) Promise.all(Array.from(cbs).map(cb => window.pywebview.api.kill_process(parseInt(cb.value)))).then(() => {
|
||||
window.pywebview.api.get_process_list().then(procs => renderDataGrid('#taskmgr_window', procs.map(p => ({ name: p.name, pid: p.pid, path: (p.path && p.path !== "None" && p.path !== "Unknown") ? p.path : "path_unknown" })), cols.process, 'pid', false));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#log_export_window .primary-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#log_export_window');
|
||||
if (checked.length === 0) return window.pywebview?.api.show_alert(getMsg("title_prompt"), getMsg("msg_sel_log_export"), "warning");
|
||||
window.pywebview?.api.export_logs(checked).then(res => res && window.pywebview.api.show_alert(getMsg("title_prompt"), getMsg("msg_export_success"), "info"));
|
||||
});
|
||||
|
||||
document.querySelector('#log_export_window .danger-btn')?.addEventListener('click', () => {
|
||||
const checked = getCheckedValues('#log_export_window');
|
||||
if (checked.length === 0 || !window.pywebview) return;
|
||||
window.pywebview.api.show_confirm(getMsg("title_prompt"), getMsg("msg_del_log_confirm")).then(res => {
|
||||
if (res) window.pywebview.api.clear_logs(checked).then(() => {
|
||||
window.pywebview.api.get_logs().then(logs => renderDataGrid('#log_export_window', logs.reverse().map(log => ({ time_str: `[${log.time_str}]`, level: log.level, action: log.action, source: log.source || '-', id: log.id })), cols.log, 'id', true));
|
||||
window.pywebview.api.show_alert(getMsg("title_prompt"), getMsg("msg_log_deleted"), "info");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('minimize_button')?.addEventListener('click', () => window.pywebview?.api.minimize());
|
||||
document.getElementById('close_button')?.addEventListener('click', () => window.pywebview?.api.hide_window());
|
||||
|
||||
const contextMenu = document.createElement('div');
|
||||
contextMenu.className = 'custom-context-menu';
|
||||
contextMenu.innerHTML = `<div class="custom-context-menu-item" id="ctx_open_location"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg><span data-i18n="ctx_open_loc"></span></div>`;
|
||||
document.body.appendChild(contextMenu);
|
||||
let ctxTarget = null;
|
||||
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const li = e.target.closest('.manage-list-item');
|
||||
if (li) {
|
||||
const cb = li.querySelector('input[type="checkbox"]');
|
||||
if (cb && cb.dataset.path && cb.dataset.path !== "path_unknown") {
|
||||
e.preventDefault();
|
||||
ctxTarget = cb.dataset.path;
|
||||
const span = contextMenu.querySelector('span');
|
||||
span.textContent = getMsg('ctx_open_loc');
|
||||
contextMenu.style.left = `${e.clientX}px`;
|
||||
contextMenu.style.top = `${e.clientY}px`;
|
||||
contextMenu.classList.add('show');
|
||||
const rect = contextMenu.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) contextMenu.style.left = `${window.innerWidth - rect.width - 5}px`;
|
||||
if (rect.bottom > window.innerHeight) contextMenu.style.top = `${window.innerHeight - rect.height - 5}px`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
contextMenu.classList.remove('show');
|
||||
});
|
||||
document.addEventListener('click', (e) => { if (!e.target.closest('.custom-context-menu')) contextMenu.classList.remove('show'); });
|
||||
document.addEventListener('scroll', () => contextMenu.classList.remove('show'), true);
|
||||
document.getElementById('ctx_open_location').addEventListener('click', () => {
|
||||
if (window.pywebview && ctxTarget) window.pywebview.api.open_file_location(ctxTarget);
|
||||
contextMenu.classList.remove('show');
|
||||
});
|
||||
|
||||
initI18nKeys();
|
||||
|
||||
const bindSettingAction = (key, action) => {
|
||||
const item = Array.from(document.querySelectorAll('#about_window .list-item, #setting_window .list-item')).find(el => {
|
||||
const h2 = el.querySelector('h2');
|
||||
return h2 && h2.getAttribute('data-i18n') === key;
|
||||
});
|
||||
|
||||
if (item) {
|
||||
const btn = item.querySelector('button');
|
||||
|
||||
if (btn) {
|
||||
btn.addEventListener('click', action);
|
||||
} else {
|
||||
item.style.cursor = 'pointer';
|
||||
item.dataset.action = key;
|
||||
item.addEventListener('click', action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bindSettingAction('web_official', () => window.pywebview?.api.open_website());
|
||||
bindSettingAction('update_check', () => {
|
||||
window.pywebview?.api.check_update().then(res => {
|
||||
if (res.error) window.pywebview.api.show_alert(getMsg("title_error"), getMsg("msg_update_fail"), "error");
|
||||
else if (res.has_update) window.pywebview.api.show_confirm(getMsg("title_prompt"), `${getMsg("msg_new_version")} ${res.latest}\n(${res.current} -> ${res.latest})`).then(ok => ok && window.pywebview.api.open_url(res.url));
|
||||
else window.pywebview.api.show_alert(getMsg("title_prompt"), `${getMsg("msg_latest_version")} ${res.current}`, "info");
|
||||
});
|
||||
});
|
||||
bindSettingAction('opt_reset', () => {
|
||||
window.pywebview?.api.show_confirm(getMsg("title_prompt"), getMsg("opt_reset_desc")).then(res => {
|
||||
if (res) window.pywebview.api.reset_config().then(() => window.pywebview.api.show_alert(getMsg("title_prompt"), getMsg("msg_reset_success"), "info"));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('select.modern-select').forEach(buildCustomSelectElement);
|
||||
renderDataGrid('#scan_window', appState.virusResults, cols.virus, 'path', true);
|
||||
renderDataGrid('#taskmgr_window', [], cols.process, 'pid', false);
|
||||
renderDataGrid('#startup_window', [], cols.startup, 'id', false);
|
||||
renderDataGrid('#junk_window', [], cols.junk, 'path', true);
|
||||
renderDataGrid('#repair_window', [], cols.repair, 'id', true);
|
||||
renderDataGrid('#whitelist_window', [], cols.pathOnly, 'path', false);
|
||||
renderDataGrid('#quarantine_window', [], cols.pathOnly, 'path', false);
|
||||
renderDataGrid('#custom_protect_window', [], cols.pathOnly, 'path', false);
|
||||
renderDataGrid('#popup_window', [], cols.popup, 'value', false);
|
||||
renderDataGrid('#log_export_window', [], cols.log, 'id', true);
|
||||
|
||||
window.addEventListener('pywebviewready', () => {
|
||||
window.pywebview.api.get_config().then(cfg => {
|
||||
const theme = cfg.theme || "white_switch";
|
||||
const lang = cfg.language || "english_switch";
|
||||
appState.firstLaunch = cfg.first_launch;
|
||||
applyTheme(theme);
|
||||
translateText(lang);
|
||||
updateCustomSelectUI('theme_select', theme);
|
||||
updateCustomSelectUI('lang_select', lang);
|
||||
|
||||
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
|
||||
document.querySelectorAll('.toggle-switch input').forEach((toggle, index) => {
|
||||
if (appState.firstLaunch) {
|
||||
toggle.checked = false;
|
||||
toggle.disabled = true;
|
||||
} else if (switchMap[index]) {
|
||||
toggle.checked = !!cfg[switchMap[index]];
|
||||
}
|
||||
});
|
||||
|
||||
window.pywebview.api.init_ui_ready();
|
||||
}).catch(err => {
|
||||
console.error("Config load failed:", err);
|
||||
window.pywebview.api.init_ui_ready();
|
||||
});
|
||||
});
|
||||
|
||||
window.onEngineReady = async () => {
|
||||
if (!appState.firstLaunch) return;
|
||||
await window.pywebview.api.update_config("first_launch", false);
|
||||
const switchMap = ["process_switch", "document_switch", "system_switch", "driver_switch", "network_switch", "sensitive_switch", "extension_switch", "cloud_switch", "context_switch"];
|
||||
const seq = ["cloud_switch", "process_switch", "document_switch", "system_switch", "network_switch", "driver_switch"];
|
||||
|
||||
for (const key of seq) {
|
||||
const index = switchMap.indexOf(key);
|
||||
if (index !== -1) {
|
||||
const toggle = document.querySelectorAll('.toggle-switch input')[index];
|
||||
if (toggle) {
|
||||
toggle.checked = true;
|
||||
try {
|
||||
const res = await window.pywebview.api.update_config(key, true);
|
||||
if (res !== undefined && res !== null) toggle.checked = res;
|
||||
} catch { toggle.checked = false; }
|
||||
finally { toggle.disabled = false; }
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.toggle-switch input').forEach(t => t.disabled = false);
|
||||
appState.firstLaunch = false;
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,449 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PYAS Security</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<script defer src="/static/js/i18n.js"></script>
|
||||
<script defer src="/static/js/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading_overlay">
|
||||
<svg class="spinner" viewBox="0 0 50 50">
|
||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="app-container" style="opacity: 0; transition: opacity 0.5s ease;">
|
||||
<header class="navigate-window pywebview-drag-region">
|
||||
<div class="navigate-title" data-i18n>PYAS Security</div>
|
||||
<div class="window-controls">
|
||||
<button class="ctrl-btn" id="about_button" data-target="about_window">
|
||||
<svg viewBox="0 0 24 24" class="theme-svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
</button>
|
||||
<button class="ctrl-btn" id="minimize_button">
|
||||
<svg viewBox="0 0 24 24" class="theme-svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
</button>
|
||||
<button class="ctrl-btn" id="close_button">
|
||||
<svg viewBox="0 0 24 24" class="theme-svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-layout">
|
||||
<aside class="options-window pywebview-drag-region">
|
||||
<button class="nav-btn active" data-target="home_window">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-target="scan_window">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-target="tool_window">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
|
||||
</button>
|
||||
<button class="nav-btn" data-target="protect_window">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="nav-btn" data-target="setting_window">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-window">
|
||||
<section id="home_window" class="page active">
|
||||
<div class="home-top-widget">
|
||||
<div class="state-widget">
|
||||
<svg viewBox="0 0 24 24" class="state-icon theme-svg" style="width: 120px; height: 120px;" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 12 11 14 15 10"></polyline></svg>
|
||||
<h2 class="state-title" data-i18n>此裝置已受到防護</h2>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<div class="list-item" style="cursor: pointer;" id="quick_scan_btn" data-action="scan">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>智能掃描</h2><p data-i18n>此選項可以快速掃描裝置安全性</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="log_export_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>日誌導出</h2><p data-i18n>此選項可以篩選日誌並匯出存檔</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-widget">
|
||||
<textarea class="log-text" readonly></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="scan_window" class="page">
|
||||
<div class="progress-widget">
|
||||
<div class="header-group">
|
||||
<h2 class="section-title" data-i18n>病毒掃描</h2>
|
||||
<p class="section-desc" id="progress_text" data-i18n>此選項可以選擇進階掃描方式</p>
|
||||
</div>
|
||||
<div class="action-group modern-action-group">
|
||||
<select id="scan_method_select" class="modern-select">
|
||||
<option value="none" selected disabled data-i18n>選擇</option>
|
||||
<option value="smart" data-i18n>智能掃描</option>
|
||||
<option value="file" data-i18n>檔案掃描</option>
|
||||
<option value="path" data-i18n>路徑掃描</option>
|
||||
<option value="full" data-i18n>全盤掃描</option>
|
||||
</select>
|
||||
<button class="action-btn modern-btn hidden" id="stop_btn" data-i18n>停止</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scan-content">
|
||||
<div class="virus-widget">
|
||||
<ul class="virus-list" id="virus_list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="protect_window" class="page">
|
||||
<div class="list-widget">
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>進程防護</h2><p data-i18n>此選項可以終止惡意進程運行</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>檔案防護</h2><p data-i18n>此選項可以隔離惡意檔案建立</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>系統防護</h2><p data-i18n>此選項可以實時修復系統異常</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>驅動防護</h2><p data-i18n>此選項可以實時攔截惡意行為</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>網路防護</h2><p data-i18n>此選項可以攔截惡意網路連接</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="custom_protect_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>自訂防護</h2><p data-i18n>此選項可以導入自訂防護規則</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tool_window" class="page">
|
||||
<div class="list-widget">
|
||||
<div class="list-item" style="cursor: pointer;" data-target="taskmgr_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>進程管理</h2><p data-i18n>此選項可以實時查看系統進程</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="startup_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>啟動項管理</h2><p data-i18n>此選項可以管理各應用啟動項</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="junk_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>垃圾清理</h2><p data-i18n>此選項可以清理暫存垃圾檔案</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="repair_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>系統修復</h2><p data-i18n>此選項可以修復系統登錄檔項目</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="popup_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>彈窗攔截</h2><p data-i18n>此選項可以選擇指定窗口攔截</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="whitelist_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>白名單</h2><p data-i18n>此選項可以查看並管理白名單</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
<div class="list-item" style="cursor: pointer;" data-target="quarantine_window">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>隔離區</h2><p data-i18n>此選項可以查看並管理隔離區</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="setting_window" class="page">
|
||||
<div class="list-widget">
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>增強模式</h2><p data-i18n>此選項可以增強靜態掃描引擎</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>擴展引擎</h2><p data-i18n>此選項可以使用擴展規則引擎</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>雲端服務</h2><p data-i18n>此選項可以自動上報可疑檔案</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>選單掃描</h2><p data-i18n>此選項可以啟用右鍵掃描功能</p>
|
||||
</div>
|
||||
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>顯色主題</h2><p data-i18n>此選項可以選擇主題顏色</p>
|
||||
</div>
|
||||
<select id="theme_select" class="modern-select">
|
||||
<option value="system_switch" selected data-i18n>跟隨系統</option>
|
||||
<option value="white_switch" data-i18n>白色主題</option>
|
||||
<option value="red_switch" data-i18n>紅色主題</option>
|
||||
<option value="yellow_switch" data-i18n>黃色主題</option>
|
||||
<option value="green_switch" data-i18n>綠色主題</option>
|
||||
<option value="blue_switch" data-i18n>藍色主題</option>
|
||||
<option value="black_switch" data-i18n>黑色主題</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>顯示語言</h2><p data-i18n>此選項可以選擇顯示語言</p>
|
||||
</div>
|
||||
<select id="lang_select" class="modern-select">
|
||||
<option value="english_switch">English</option>
|
||||
<option value="traditional_switch">繁體中文</option>
|
||||
<option value="simplified_switch">简体中文</option>
|
||||
<option value="japanese_switch">日本語</option>
|
||||
<option value="korean_switch">한국어</option>
|
||||
<option value="french_switch">Français</option>
|
||||
<option value="spanish_switch">Español</option>
|
||||
<option value="hindi_switch">हिन्दी</option>
|
||||
<option value="arabic_switch">العربية</option>
|
||||
<option value="russian_switch">Русский</option>
|
||||
<option value="slovenian_switch">Slovenščina</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>重置選項</h2><p data-i18n>此選項可以重置所有設定</p>
|
||||
</div>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>執行</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about_window" class="page">
|
||||
<div class="list-widget">
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>產品金鑰</h2><p data-i18n>此選項可以啟用產品金鑰</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg></button>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>官方網站</h2><p data-i18n>此選項可以前往官方網站</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg></button>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="item-info">
|
||||
<h2 data-i18n>檢查更新</h2><p data-i18n>此選項可以檢查可用更新</p>
|
||||
</div>
|
||||
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg></button>
|
||||
</div>
|
||||
<div class="license-widget">
|
||||
<textarea class="license-text" readonly>87owo Publicly Available Source Code License Agreement
|
||||
Last updated: 2025-12-19
|
||||
|
||||
1. License and Ownership
|
||||
- This software and its source code are source available for public view.
|
||||
- Access or execution of certain features may require an activation key.
|
||||
- 87owo reserves all legal rights, no intellectual property is transferred.
|
||||
- All project archives, source code, and binaries remain fully proprietary.
|
||||
|
||||
2. Use and Prohibitions
|
||||
- You may view, download, and modify the code for local and personal use.
|
||||
- Redistribution of any original or modified code requires prior consent.
|
||||
- Commercial hosting or SaaS deployment is prohibited without permission.
|
||||
- Reverse engineering, license removal, or illegal use is strictly barred.
|
||||
|
||||
3. Data and Privacy
|
||||
- This project may collect operational data for optimization and analysis.
|
||||
- All collected data is strictly anonymized to ensure the user's privacy.
|
||||
- Transmission of information is encrypted to prevent unauthorized access.
|
||||
|
||||
4. Updates and Terms
|
||||
- New releases may include critical patches, enhancements, or UI changes.
|
||||
- Continued use of these services constitutes acceptance of updated terms.
|
||||
- The author reserves the right to modify these license terms at any time.
|
||||
|
||||
5. Warranty and Liability
|
||||
- All services and assets are provided "as is" without any form of warranty.
|
||||
- 87owo is not liable for any direct or indirect damages from software use.
|
||||
- Users assume all risks regarding performance, data, and system integrity.
|
||||
|
||||
© 2020-2026 87owo (PYAS Security). All rights reserved.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="taskmgr_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>進程管理</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋進程..." placeholder="搜尋進程...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>查看</button>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>結束</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="startup_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>啟動項管理</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋項目..." placeholder="搜尋項目...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>啟用</button>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>禁用</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="whitelist_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>白名單管理</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋檔案..." placeholder="搜尋檔案...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>加入</button>
|
||||
<button class="action-btn modern-btn" data-i18n>移除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="quarantine_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>隔離區管理</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋檔案..." placeholder="搜尋檔案...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>加入</button>
|
||||
<button class="action-btn modern-btn" data-i18n>移除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="junk_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>垃圾清理</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋檔案..." placeholder="搜尋檔案...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>掃描</button>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>刪除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="repair_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>系統修復</h2>
|
||||
<div class="spacer"></div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>掃描</button>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>修復</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="popup_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="tool_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>彈窗攔截</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋窗口..." placeholder="搜尋窗口...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>加入</button>
|
||||
<button class="action-btn modern-btn" data-i18n>移除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="custom_protect_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="protect_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>自訂防護</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋規則..." placeholder="搜尋規則...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>加入</button>
|
||||
<button class="action-btn modern-btn" data-i18n>移除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
|
||||
<section id="log_export_window" class="page sub-page">
|
||||
<div class="sub-header-toolbar">
|
||||
<button class="icon-btn" data-target="home_window"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
|
||||
<h2 data-i18n>日誌導出</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="text" data-i18n-placeholder="搜尋日誌..." placeholder="搜尋日誌...">
|
||||
</div>
|
||||
<button class="action-btn modern-btn primary-btn" data-i18n>導出</button>
|
||||
<button class="action-btn modern-btn danger-btn" data-i18n>刪除</button>
|
||||
</div>
|
||||
<div class="manage-widget flex-grow"><ul class="manage-list"></ul></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
translate_dict = {
|
||||
"traditional_switch": {"开启":"開啟","关闭":"關閉","On":"開啟","Off":"關閉"},
|
||||
|
||||
"simplified_switch": {"開啟":"开启","關閉":"关闭","On":"开启","Off":"关闭",
|
||||
"錯誤":"错误","發現":"发现","關閉":"关闭","裝置":"设备","記憶體":"内存",
|
||||
"軟體":"软件","檔案":"文件","確定":"确认","設定":"设置","關於":"关于",
|
||||
"總數":"总数","註冊":"注册","圖標":"图标","路徑":"路径","網路":"网络",
|
||||
"位元":"字节","靈敏":"敏感","複製":"复制","開啟":"开启","靜態":"静态",
|
||||
"訊":"讯","項":"项","掃":"扫","試":"试","黃":"黄","嗎":"吗","防":"保",
|
||||
"鑰":"钥","當":"当","後":"后","護":"护","導":"导","區":"区","復":"复",
|
||||
"攔":"拦","請":"请","統":"统","刪":"删","進":"进","個":"个","選":"选",
|
||||
"啟":"启","監":"监","並":"并","徑":"径","盤":"盘","隱":"隐","動":"动",
|
||||
"狀":"状","誌":"志","這":"这","會":"会","題":"题","擇":"择","語":"语",
|
||||
"無":"无","顯":"显","變":"变","戶":"户","碼":"码","雲":"云","介":"界",
|
||||
"稱":"称","類":"类","輸":"输","執":"执","數":"数","尋":"寻","網":"网",
|
||||
"詢":"询","製":"制","員":"员","許":"许","條":"条","藍":"蓝","紅":"红",
|
||||
"綠":"绿","態":"态","務":"务","連":"连","檢":"检","測":"测","關":"关",
|
||||
"強":"强","驅":"驱","為":"为","腳":"脚","單":"单","開":"开","擴":"扩",
|
||||
"惡":"恶","據":"据","庫":"库","經":"经","敗":"败","謝":"谢","別":"别",
|
||||
"載":"载","誤":"误","報":"报","鏈":"链","竄":"窜","蹤":"踪","與":"与",
|
||||
"異":"异","實":"实","時":"时","彈":"弹","窗":"窗","暫":"暂","訂":"订",
|
||||
"資":"资","產":"产","金":"密","離":"离","規":"规","則":"则","來":"来",
|
||||
"殼":"壳","碼":"码","線":"线","餘":"余",},
|
||||
|
||||
"english_switch": {"開啟":"On","關閉":"Off","开启":"On","关闭":"Off",",":", ",
|
||||
"立即刪除":"Delete Now","安全模式":"Safe Mode","官方網站":"Official Website ",
|
||||
"腳本":"Script ","鏈式":"Chain ","記憶體":"Memory ","正在加載":"Loading ","加載":"Load ",
|
||||
"行為":"Behavior ","可疑":"Suspicious ","勒索":"Ransom ","主要":"Main ",
|
||||
"許可條款":"License Terms ","惡意":"Malicious", "開發":"Developers ",
|
||||
"啟用":"Enable ","禁用":"Disable ","正在初始化中":"Initializing ",
|
||||
"正在掃描":"Scanning ","正在刪除":"Deleting ","正在追蹤":"Tracking ",
|
||||
"最小化到":"Minimize To ","靈敏度":"Sensitivity ","正在檢查":"Checking ",
|
||||
"確定要":"Sure Want To ","引擎靈敏度":"Sensitivity ","更新":"Update ",
|
||||
"白色主題":"White","黃色主題":"Yellow","紅色主題":"Red","語言":"Language ",
|
||||
"藍色主題":"Blue","綠色主題":"Green","自訂主題":"Custom","黑色主題":"Black",
|
||||
"特別感謝":"Contributors ","版本資訊":"Information","通訊":"Connect ",
|
||||
"註冊":"Registry ","退出":"Quit ","檔案":"File ","當前":"Currently ",
|
||||
"狀態":"State ","掃描":"Scan ","工具":"Tools ","防護":"Protect ",
|
||||
"失敗":"Failed ","請":"Please ","路徑":"Path ","方式":"Method ",
|
||||
"需要":"Need ","病毒":"Virus ","選擇":"Select ","返回":"Back",
|
||||
"受到":"Been ","錯誤":"Error","完成":"Complete ","引擎":"Engine ",
|
||||
"托盤":"Tray ","圖標":"Icon ","不安全":"Unsafe","增強":"Enhance ",
|
||||
"系統":"System ","雲端":"Cloud ","重啟":"Reboot ","修復":"Repair ",
|
||||
"裝置":"Device ","發現":"Found ","成功":"Success ","清理":"Cleanup ",
|
||||
"確定":"OK","執行":"Execute ","警告":"Warning ","使用":"Use ",
|
||||
"網路":"Network ","變更":"Change ","總數":"Total","結束":"End ",
|
||||
"選項":"Options ","監控":"Monitor ","連接":"Connect ","可以":"Can ",
|
||||
"進程":"Process ","攔截":"Block ","引導":"Boot ","刪除":"Delete ",
|
||||
"分區":"Partition ","設定":"Setting ","關於":"About","模式":"Mode ",
|
||||
"提高":"Improve ","安全":"Security ","顯色":"Color ","顏色":"Color ",
|
||||
"主題":"Theme ", "回收":"Collect ", "無用":"Useless ","導入":"Import ",
|
||||
"風險":"Risk ","介面":"UI ","核心":"Core ","服務":"Services ",
|
||||
"管理":"Manage","所有":"All ","您":"You ","取消":"Cancel ","未":"Not ",
|
||||
"垃圾":"Junk ","位元":"byte ","本機":"Computer ","停止":"Stop ",
|
||||
"到":"To ","白名單":"White List ","軟體":"Software ","正在":"",
|
||||
"增加":"Add ","移除":"Delete ","項目":"Items ","生成":"Create ","稱":"",
|
||||
"複製":"Copy ","此":"This ","並":"And ","全盤":"Full ","最新":"Latest ",
|
||||
"已存在":"Already ","數據庫":"Database ","誤報":"False Positive ",
|
||||
"高":"High ","新":"New ","和":"And ","可能會":"Possible ","會":"Can ",
|
||||
"日誌":"Log","已":"Has ","彈窗":"Window ","要":"To ","驅動":"Driver ",
|
||||
"的":"","顯示":"","個":"","嗎":"","了":"","行":"","立即":"","規則":"Rule ",
|
||||
"密鑰":"Key ","檢測":"Detect ","製作":"Maker ","運行":"Running",
|
||||
"擴展":"Extension ","套件":"Kit ","自動":"Auto ","上報":"Report ",
|
||||
"分析":"Analysis ","自身":"Self ","其他":"Other ","配置":"Config ",
|
||||
"丟失":"Lost ","網站":"Website ","不兼容":"Not Compatible ","列表":"List ",
|
||||
"竄改":"Tamper ","第三方":"Other ","實時":"Real Time ","查看":"View ",
|
||||
"指定":"Specify ","清空":"Empty ","資源回收桶":"Recycle Bin ", "無法":"Cannot ",
|
||||
"與":"With ","當":"When ","功能":"Function ","異常":"Abnormal ", "來":"To ",
|
||||
"重置":"Reset ","窗口":"Window ","暫存":"Temp ","自訂":"Custom ",
|
||||
"不支持":"Not Support ","共":"Total ","秒":"Seconds ","耗時":"Takes ",
|
||||
"沙箱":"Sandbox ", "虛擬":"Virtual ", "隔離":"Quarantine ", "時":"",
|
||||
"建立":"Create ","環境":"Environment ","沙箱":"Sandbox ","化":"", "表":"",
|
||||
"達":"Reached ", "上限":"Limit ", "區":"", "金鑰":"Key ", "產品":"Product ",
|
||||
"運行庫":".NET CLR ","殼碼":"Shellcode ","線程":"Thread ","注入":"Inject ",
|
||||
"前往":"Go ","檢查":"Check ","可用":"Available ","靜態":"Static ",
|
||||
"版本":"Version ","是":"","剩餘":"Remain ","遠控":"Remote ","監控":"Monitor ",
|
||||
" ":" "},
|
||||
|
||||
"white_switch": {
|
||||
"_white": "_black",},
|
||||
"red_switch": {
|
||||
"235, 235, 235": "255, 235, 235",
|
||||
"245, 245, 245": "255, 225, 225",
|
||||
"255, 255, 255": "255, 245, 245",
|
||||
"240, 240, 240": "255, 220, 220",
|
||||
"250, 250, 250": "255, 210, 210",
|
||||
"220, 255, 220": "255, 220, 220",
|
||||
"210, 255, 210": "255, 210, 210",
|
||||
"_white": "_black"
|
||||
},
|
||||
"yellow_switch": {
|
||||
"235, 235, 235": "255, 255, 235",
|
||||
"245, 245, 245": "255, 255, 225",
|
||||
"255, 255, 255": "255, 255, 245",
|
||||
"240, 240, 240": "255, 255, 220",
|
||||
"250, 250, 250": "255, 255, 210",
|
||||
"220, 255, 220": "255, 255, 220",
|
||||
"210, 255, 210": "255, 255, 210",
|
||||
"_white": "_black"
|
||||
},
|
||||
"green_switch": {
|
||||
"235, 235, 235": "235, 255, 235",
|
||||
"245, 245, 245": "225, 255, 225",
|
||||
"255, 255, 255": "245, 255, 245",
|
||||
"240, 240, 240": "220, 255, 220",
|
||||
"250, 250, 250": "210, 255, 210",
|
||||
"_white": "_black"
|
||||
},
|
||||
"blue_switch": {
|
||||
"235, 235, 235": "235, 235, 255",
|
||||
"245, 245, 245": "225, 225, 255",
|
||||
"255, 255, 255": "245, 245, 255",
|
||||
"240, 240, 240": "220, 220, 255",
|
||||
"250, 250, 250": "210, 210, 255",
|
||||
"220, 255, 220": "220, 220, 255",
|
||||
"210, 255, 210": "210, 210, 255",
|
||||
"_white": "_black"
|
||||
},
|
||||
"black_switch": {
|
||||
"255, 255, 255": "20, 20, 20",
|
||||
"245, 245, 245": "40, 40, 40",
|
||||
"235, 235, 235": "30, 30, 30",
|
||||
"240, 240, 240": "70, 70, 70",
|
||||
"250, 250, 250": "80, 80, 80",
|
||||
"220, 255, 220": "70, 70, 70",
|
||||
"210, 255, 210": "80, 80, 80",
|
||||
"0, 0, 0": "255, 255, 255",
|
||||
"_black": "_white"
|
||||
}
|
||||
}
|
||||
+726
-150
@@ -1,8 +1,5 @@
|
||||
import os, yara, time, numpy, base64, requests
|
||||
import ctypes, ctypes.wintypes, hashlib, onnxruntime
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import os, re, yara, time, math, json, numpy, base64, datetime, requests, zlib
|
||||
import ctypes, ctypes.wintypes, hashlib, pefile, threading, onnxruntime
|
||||
|
||||
####################################################################################################
|
||||
|
||||
@@ -11,14 +8,25 @@ class GUID(ctypes.Structure):
|
||||
("Data1", ctypes.wintypes.DWORD),
|
||||
("Data2", ctypes.wintypes.WORD),
|
||||
("Data3", ctypes.wintypes.WORD),
|
||||
("Data4", ctypes.c_ubyte * 8)]
|
||||
("Data4", ctypes.c_ubyte * 8)
|
||||
]
|
||||
|
||||
class WINTRUST_FILE_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbStruct", ctypes.wintypes.DWORD),
|
||||
("pcwszFilePath", ctypes.wintypes.LPCWSTR),
|
||||
("hFile", ctypes.wintypes.HANDLE),
|
||||
("pgKnownSubject", ctypes.wintypes.LPVOID)]
|
||||
("pgKnownSubject", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA_UNION(ctypes.Union):
|
||||
_fields_ = [
|
||||
("pFile", ctypes.POINTER(WINTRUST_FILE_INFO)),
|
||||
("pCatalog", ctypes.wintypes.LPVOID),
|
||||
("pBlob", ctypes.wintypes.LPVOID),
|
||||
("pSgnr", ctypes.wintypes.LPVOID),
|
||||
("pCert", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
class WINTRUST_DATA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
@@ -28,19 +36,20 @@ class WINTRUST_DATA(ctypes.Structure):
|
||||
("dwUIChoice", ctypes.wintypes.DWORD),
|
||||
("fdwRevocationChecks", ctypes.wintypes.DWORD),
|
||||
("dwUnionChoice", ctypes.wintypes.DWORD),
|
||||
("pFile", ctypes.POINTER(WINTRUST_FILE_INFO)),
|
||||
("u", WINTRUST_DATA_UNION),
|
||||
("dwStateAction", ctypes.wintypes.DWORD),
|
||||
("hWVTStateData", ctypes.wintypes.HANDLE),
|
||||
("pwszURLReference", ctypes.wintypes.LPCWSTR),
|
||||
("dwProvFlags", ctypes.wintypes.DWORD),
|
||||
("dwUIContext", ctypes.wintypes.DWORD),
|
||||
("pSignatureSettings", ctypes.wintypes.LPVOID)]
|
||||
("pSignatureSettings", ctypes.wintypes.LPVOID)
|
||||
]
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class sign_scanner:
|
||||
def __init__(self):
|
||||
self.verify = GUID(0x00AAC56B, 0xCD44, 0x11D0, (0x8C, 0xC2, 0x00, 0xC0, 0x4F, 0xC2, 0x95, 0xEE))
|
||||
self.verify = GUID(0x00AAC56B, 0xCD44, 0x11D0, (ctypes.c_ubyte * 8)(0x8C, 0xC2, 0x00, 0xC0, 0x4F, 0xC2, 0x95, 0xEE))
|
||||
|
||||
def init_windll(self, path):
|
||||
for name in path:
|
||||
@@ -48,7 +57,6 @@ class sign_scanner:
|
||||
setattr(self, name.lower(), ctypes.WinDLL(name, use_last_error=True))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.WinVerifyTrust = self.wintrust.WinVerifyTrust
|
||||
self.WinVerifyTrust.restype = ctypes.wintypes.LONG
|
||||
@@ -56,15 +64,34 @@ class sign_scanner:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def sign_verify(self, file_path):
|
||||
if os.name != 'nt':
|
||||
return False
|
||||
try:
|
||||
fi = WINTRUST_FILE_INFO(ctypes.sizeof(WINTRUST_FILE_INFO), file_path, None, None)
|
||||
data = WINTRUST_DATA(ctypes.sizeof(WINTRUST_DATA), None, None, 2, 0, 1,
|
||||
ctypes.pointer(fi), 1, None, None, 0, 0, None)
|
||||
s = self.WinVerifyTrust(None, ctypes.byref(self.verify), ctypes.byref(data))
|
||||
data.dwStateAction = 2
|
||||
self.WinVerifyTrust(None, ctypes.byref(self.verify), ctypes.byref(data))
|
||||
return s == 0
|
||||
fi = WINTRUST_FILE_INFO()
|
||||
fi.cbStruct = ctypes.sizeof(WINTRUST_FILE_INFO)
|
||||
fi.pcwszFilePath = os.path.abspath(file_path)
|
||||
fi.hFile = None
|
||||
fi.pgKnownSubject = None
|
||||
wt_union = WINTRUST_DATA_UNION()
|
||||
wt_union.pFile = ctypes.pointer(fi)
|
||||
td = WINTRUST_DATA()
|
||||
td.cbStruct = ctypes.sizeof(WINTRUST_DATA)
|
||||
td.pPolicyCallbackData = None
|
||||
td.pSIPClientData = None
|
||||
td.dwUIChoice = 2
|
||||
td.fdwRevocationChecks = 0
|
||||
td.dwUnionChoice = 1
|
||||
td.u = wt_union
|
||||
td.dwStateAction = 0
|
||||
td.hWVTStateData = None
|
||||
td.pwszURLReference = None
|
||||
td.dwProvFlags = 0
|
||||
td.dwUIContext = 0
|
||||
td.pSignatureSettings = None
|
||||
return self.WinVerifyTrust(None, ctypes.byref(self.verify), ctypes.byref(td)) == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -94,6 +121,8 @@ class rule_scanner:
|
||||
if yara_files:
|
||||
self.compile_all_rules(yara_files)
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def load_compiled_rule(self, file):
|
||||
try:
|
||||
self.rules = yara.load(file)
|
||||
@@ -113,6 +142,8 @@ class rule_scanner:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def yara_scan(self, file_path):
|
||||
try:
|
||||
if not self.rules:
|
||||
@@ -121,22 +152,50 @@ class rule_scanner:
|
||||
matches = self.rules.match(filepath=file_path)
|
||||
if matches:
|
||||
rule_name = str(matches[0])
|
||||
types = rule_name.split("_")[0]
|
||||
label = rule_name.split("_")[1]
|
||||
level = rule_name.split("_")[2]
|
||||
return f"{types}/{label}", level
|
||||
label = rule_name.split("_")[0]
|
||||
level = rule_name.split("_")[-1]
|
||||
return f"{label}:WinPE/Unknown.{level}!yr", level
|
||||
return False, False
|
||||
except Exception:
|
||||
return False, False
|
||||
|
||||
def yara_mem_scan(self, pid):
|
||||
try:
|
||||
if not self.rules:
|
||||
return False, False
|
||||
|
||||
matches = self.rules.match(pid=pid)
|
||||
if matches:
|
||||
rule_name = str(matches[0])
|
||||
label = rule_name.split("_")[0]
|
||||
level = rule_name.split("_")[-1]
|
||||
return f"{label}:WinPE/Unknown.{level}!ym", level
|
||||
return False, False
|
||||
except Exception:
|
||||
return False, False
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class model_scanner:
|
||||
class pe_scanner:
|
||||
_STRING_PATTERN = re.compile(b'[\x20-\x7E]{5,}')
|
||||
|
||||
_BYTE_HIST_KEYS = [f'ByteHist_{i:02X}' for i in range(256)]
|
||||
_BYTE_ENT_KEYS = [f'ByteEnt_{i:02X}' for i in range(256)]
|
||||
_STRING_KEYS = [f'StringHist_{i:02d}' for i in range(95)]
|
||||
_SECTION_HASH_KEYS = [f'SectionHash_{i:02d}' for i in range(50)]
|
||||
_CHAR_FLAGS = [0x00000020, 0x00000040, 0x00000080, 0x02000000, 0x20000000, 0x40000000, 0x80000000]
|
||||
_CHAR_COUNT_KEYS = {flag: f'Char_{flag:08X}_Count' for flag in _CHAR_FLAGS}
|
||||
_CHAR_ENT_KEYS = {flag: f'Char_{flag:08X}_MeanEntropy' for flag in _CHAR_FLAGS}
|
||||
|
||||
_C_LOG_C_TABLE = numpy.zeros(2049, dtype=numpy.float64)
|
||||
_C_LOG_C_TABLE[1:] = numpy.arange(1, 2049) * numpy.log2(numpy.arange(1, 2049))
|
||||
|
||||
def __init__(self):
|
||||
self.models = []
|
||||
self.labels = ["Pefile/White", "Pefile/General"]
|
||||
self.detect_set = {"Pefile/General"}
|
||||
self.resize = (224, 224)
|
||||
self.model = None
|
||||
self.input_name = None
|
||||
self.feature_order = []
|
||||
self.signer = sign_scanner()
|
||||
self.signer.init_windll(["wintrust"])
|
||||
|
||||
def load_path(self, path, callback=None):
|
||||
for root, _, files in os.walk(path):
|
||||
@@ -144,97 +203,581 @@ class model_scanner:
|
||||
full_path = os.path.join(root, file)
|
||||
if callback:
|
||||
callback(full_path)
|
||||
self.load_file(full_path)
|
||||
if full_path.endswith('.onnx'):
|
||||
self.load_model(full_path)
|
||||
|
||||
def load_file(self, file):
|
||||
if file.lower().endswith('.onnx'):
|
||||
try:
|
||||
session = onnxruntime.InferenceSession(file)
|
||||
self.models.append(session)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def model_scan(self, file_path, full_output=False):
|
||||
if not self.models:
|
||||
return (False, False) if not full_output else ([], str(file_path), None)
|
||||
|
||||
data = self.get_data(file_path)
|
||||
if not data:
|
||||
return (False, False) if not full_output else ([], str(file_path), None)
|
||||
|
||||
image = self.preprocess_image(data, self.resize)
|
||||
if not image:
|
||||
return (False, False) if not full_output else ([], str(file_path), None)
|
||||
|
||||
arr = numpy.asarray(image).astype('float32') / 255.0
|
||||
arr = numpy.expand_dims(arr, axis=0)
|
||||
if arr.ndim == 3:
|
||||
arr = numpy.expand_dims(arr, axis=-1)
|
||||
|
||||
results = []
|
||||
best_malicious = None
|
||||
|
||||
for model in self.models:
|
||||
input_meta = model.get_inputs()[0]
|
||||
input_name = input_meta.name
|
||||
input_shape = input_meta.shape
|
||||
|
||||
curr_arr = arr.copy()
|
||||
if len(input_shape) == 4:
|
||||
if input_shape[1] in (1, 3) and input_shape[3] not in (1, 3):
|
||||
curr_arr = curr_arr.transpose(0, 3, 1, 2)
|
||||
|
||||
try:
|
||||
probs = model.run(None, {input_name: curr_arr})[0]
|
||||
pred = probs[0]
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
idx = int(numpy.argmax(pred))
|
||||
label = self.labels[idx] if idx < len(self.labels) else f"Class_{idx}"
|
||||
conf = round(float(pred[idx]) * 100, 2)
|
||||
|
||||
if full_output:
|
||||
results.append(("Whole File", label, conf, self.pil_to_base64(image)))
|
||||
|
||||
if label in self.detect_set:
|
||||
if best_malicious is None or conf > best_malicious[1]:
|
||||
best_malicious = (label, int(conf))
|
||||
|
||||
if full_output:
|
||||
results.sort(key=lambda x: (x[1] not in self.detect_set, -x[2]))
|
||||
malicious_count = sum(1 for _, lbl, _, _ in results if lbl in self.detect_set)
|
||||
total = len(results)
|
||||
return results, str(file_path), f"{malicious_count}/{total}"
|
||||
|
||||
return best_malicious if best_malicious else (False, False)
|
||||
|
||||
def pil_to_base64(self, img):
|
||||
buf = BytesIO()
|
||||
img.save(buf, format='PNG')
|
||||
return base64.b64encode(buf.getvalue()).decode()
|
||||
|
||||
def preprocess_image(self, data, size, channels=1):
|
||||
width, height = size
|
||||
wah = int(numpy.ceil(numpy.sqrt(len(data) / channels)))
|
||||
arr = numpy.frombuffer(data, dtype=numpy.uint8)
|
||||
imgbuf = numpy.zeros(wah * wah * channels, dtype=numpy.uint8)
|
||||
imgbuf[:len(arr)] = arr
|
||||
image = Image.fromarray(imgbuf.reshape((wah, wah)), 'L')
|
||||
return image.resize((width, height), Image.Resampling.NEAREST)
|
||||
|
||||
def get_data(self, file_path):
|
||||
def load_model(self, model_path):
|
||||
if not os.path.exists(model_path):
|
||||
return
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
return f.read()
|
||||
self.model = onnxruntime.InferenceSession(model_path, providers=['CPUExecutionProvider'])
|
||||
self.input_name = self.model.get_inputs()[0].name
|
||||
feat_path = os.path.join(os.path.dirname(model_path), "features.json")
|
||||
if os.path.exists(feat_path):
|
||||
with open(feat_path, 'r', encoding='utf-8') as f:
|
||||
self.feature_order = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _safe_float(self, val):
|
||||
try:
|
||||
f = float(val)
|
||||
if math.isinf(f) or math.isnan(f):
|
||||
return 0.0
|
||||
return f
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def _calc_entropy(self, data):
|
||||
if not data:
|
||||
return 0.0
|
||||
arr = numpy.frombuffer(data, dtype=numpy.uint8)
|
||||
sz = len(arr)
|
||||
counts = numpy.bincount(arr, minlength=256)
|
||||
counts = counts[counts > 0]
|
||||
return float(numpy.log2(sz) - numpy.sum(counts * numpy.log2(counts)) / sz)
|
||||
|
||||
def _extract_strings(self, file_bytes):
|
||||
res = {"StringCount": 0.0, "StringMeanLength": 0.0, "StringEntropy": 0.0}
|
||||
res.update(dict.fromkeys(self._STRING_KEYS, 0.0))
|
||||
|
||||
matches = self._STRING_PATTERN.findall(file_bytes)
|
||||
if not matches:
|
||||
return res
|
||||
|
||||
combined_data = b''.join(matches)
|
||||
count = len(matches)
|
||||
total_len = len(combined_data)
|
||||
|
||||
res["StringCount"] = float(count)
|
||||
res["StringMeanLength"] = float(total_len) / count
|
||||
res["StringEntropy"] = self._calc_entropy(combined_data)
|
||||
|
||||
arr = numpy.frombuffer(combined_data, dtype=numpy.uint8)
|
||||
counts = numpy.bincount(arr, minlength=127)
|
||||
char_counts = counts[0x20:0x7F]
|
||||
|
||||
res.update(dict(zip(self._STRING_KEYS, char_counts.astype(numpy.float64) / total_len)))
|
||||
return res
|
||||
|
||||
def _extract_histograms(self, file_bytes):
|
||||
arr = numpy.frombuffer(file_bytes, dtype=numpy.uint8)
|
||||
sz = len(arr)
|
||||
res = {}
|
||||
|
||||
if sz == 0:
|
||||
res.update(dict.fromkeys(self._BYTE_HIST_KEYS, 0.0))
|
||||
res.update(dict.fromkeys(self._BYTE_ENT_KEYS, 0.0))
|
||||
return res
|
||||
|
||||
byte_counts = numpy.bincount(arr, minlength=256)
|
||||
byte_hist = byte_counts.astype(numpy.float64) / sz
|
||||
res.update(dict(zip(self._BYTE_HIST_KEYS, byte_hist)))
|
||||
|
||||
ent_hist = numpy.zeros(256, dtype=numpy.float64)
|
||||
window = 2048
|
||||
step = 1024
|
||||
|
||||
if sz < window:
|
||||
w_counts = byte_counts
|
||||
p = w_counts[w_counts > 0] / float(sz)
|
||||
entropy = float(-numpy.sum(p * numpy.log2(p)))
|
||||
ent_bin = min(int(entropy * 2.0), 15)
|
||||
byte_bins = w_counts.reshape(16, 16).sum(axis=1)
|
||||
idx_start = ent_bin * 16
|
||||
ent_hist[idx_start : idx_start+16] += byte_bins
|
||||
else:
|
||||
num_windows = (sz - window) // step + 1
|
||||
batch_windows = 10000
|
||||
|
||||
for w_start in range(0, num_windows, batch_windows):
|
||||
w_end = min(w_start + batch_windows, num_windows)
|
||||
b_num = w_end - w_start
|
||||
|
||||
start_byte = w_start * step
|
||||
end_byte = start_byte + b_num * step + step
|
||||
b_arr = arr[start_byte:end_byte]
|
||||
|
||||
arr_2d = b_arr.reshape(b_num + 1, step)
|
||||
offsets = (numpy.arange(b_num + 1, dtype=numpy.int32)[:, None] * 256)
|
||||
flat_idx = (arr_2d.astype(numpy.int32) + offsets).ravel()
|
||||
|
||||
chunk_counts = numpy.bincount(flat_idx, minlength=(b_num + 1) * 256).reshape(b_num + 1, 256)
|
||||
window_counts = chunk_counts[:-1] + chunk_counts[1:]
|
||||
|
||||
sum_c_log_c = numpy.sum(self._C_LOG_C_TABLE[window_counts], axis=1)
|
||||
entropies = 11.0 - sum_c_log_c / 2048.0
|
||||
|
||||
ent_bins = (entropies * 2.0).astype(numpy.int32)
|
||||
numpy.clip(ent_bins, 0, 15, out=ent_bins)
|
||||
|
||||
byte_bins = window_counts.reshape(b_num, 16, 16).sum(axis=2)
|
||||
flat_ent_bins = (ent_bins[:, None] * 16 + numpy.arange(16)).ravel()
|
||||
numpy.add.at(ent_hist, flat_ent_bins, byte_bins.ravel())
|
||||
|
||||
ent_sum = numpy.sum(ent_hist)
|
||||
if ent_sum > 0:
|
||||
ent_hist /= ent_sum
|
||||
|
||||
res.update(dict(zip(self._BYTE_ENT_KEYS, ent_hist)))
|
||||
return res
|
||||
|
||||
def _extract_overlay_features(self, pe, fsize):
|
||||
overlay_offset = pe.get_overlay_data_start_offset()
|
||||
if not overlay_offset or overlay_offset >= fsize:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
overlay_data = pe.get_overlay()
|
||||
if not overlay_data:
|
||||
return {"HasOverlay": 0.0, "OverlaySize": 0.0, "OverlayRatio": 0.0, "OverlayEntropy": 0.0}
|
||||
|
||||
sz = len(overlay_data)
|
||||
return {
|
||||
"HasOverlay": 1.0,
|
||||
"OverlaySize": float(sz),
|
||||
"OverlayRatio": float(sz) / float(fsize),
|
||||
"OverlayEntropy": self._calc_entropy(overlay_data)
|
||||
}
|
||||
|
||||
def _extract_rich_header(self, pe):
|
||||
if not hasattr(pe, 'RICH_HEADER') or not pe.RICH_HEADER:
|
||||
return {"HasRichHeader": 0.0, "RichHeaderCount": 0.0}
|
||||
return {
|
||||
"HasRichHeader": 1.0,
|
||||
"RichHeaderCount": float(len(pe.RICH_HEADER.values) // 2) if hasattr(pe.RICH_HEADER, 'values') else 0.0
|
||||
}
|
||||
|
||||
def _extract_ep_anomalies(self, pe):
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe, 'sections') or not pe.sections:
|
||||
return {"EntryPointSectionIndex": -1.0, "EntryPointInExecutable": 0.0, "EntryPointInLastSection": 0.0}
|
||||
|
||||
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
||||
ep_section_idx = -1
|
||||
ep_in_exec = 0.0
|
||||
|
||||
for idx, sec in enumerate(pe.sections):
|
||||
if sec.VirtualAddress <= ep < (sec.VirtualAddress + sec.Misc_VirtualSize):
|
||||
ep_section_idx = idx
|
||||
if sec.Characteristics & 0x20000000:
|
||||
ep_in_exec = 1.0
|
||||
break
|
||||
|
||||
return {
|
||||
"EntryPointSectionIndex": float(ep_section_idx),
|
||||
"EntryPointInExecutable": ep_in_exec,
|
||||
"EntryPointInLastSection": 1.0 if ep_section_idx == len(pe.sections) - 1 else 0.0
|
||||
}
|
||||
|
||||
def _extract_advanced_resources(self, pe):
|
||||
res_data = {
|
||||
"ResourceMaxEntropy": 0.0,
|
||||
"ResourceMinEntropy": 0.0,
|
||||
"ResourceMeanEntropy": 0.0,
|
||||
"ResourceLangCount": 0.0,
|
||||
"ResourceRCDataCount": 0.0
|
||||
}
|
||||
|
||||
if not hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
return res_data
|
||||
|
||||
entropies = []
|
||||
langs = set()
|
||||
rcdata_count = 0
|
||||
|
||||
for entry_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if hasattr(entry_type, 'id') and entry_type.id == 10:
|
||||
rcdata_count += len(getattr(entry_type.directory, 'entries', []))
|
||||
|
||||
if not hasattr(entry_type, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_id in entry_type.directory.entries:
|
||||
if not hasattr(entry_id, 'directory'):
|
||||
continue
|
||||
|
||||
for entry_lang in entry_id.directory.entries:
|
||||
if hasattr(entry_lang, 'id'):
|
||||
langs.add(entry_lang.id)
|
||||
if hasattr(entry_lang, 'data'):
|
||||
try:
|
||||
data = pe.get_data(entry_lang.data.struct.OffsetToData, entry_lang.data.struct.Size)
|
||||
entropies.append(self._calc_entropy(data))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
res_data["ResourceLangCount"] = float(len(langs))
|
||||
res_data["ResourceRCDataCount"] = float(rcdata_count)
|
||||
|
||||
if entropies:
|
||||
res_data["ResourceMaxEntropy"] = max(entropies)
|
||||
res_data["ResourceMinEntropy"] = min(entropies)
|
||||
res_data["ResourceMeanEntropy"] = sum(entropies) / len(entropies)
|
||||
|
||||
return res_data
|
||||
|
||||
def _extract_load_config(self, pe):
|
||||
cfg = {"HasLoadConfig": 0.0, "HasCFG": 0.0, "HasSEHTable": 0.0}
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_LOAD_CONFIG'):
|
||||
cfg["HasLoadConfig"] = 1.0
|
||||
struct = pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct
|
||||
if getattr(struct, 'GuardCFFunctionTable', 0) != 0:
|
||||
cfg["HasCFG"] = 1.0
|
||||
if getattr(struct, 'SEHandlerTable', 0) != 0:
|
||||
cfg["HasSEHTable"] = 1.0
|
||||
return cfg
|
||||
|
||||
def _extract_security_directory(self, pe, file_bytes, fsize):
|
||||
res = {"HasSignature": 0.0, "SignatureCount": 0.0}
|
||||
if not hasattr(pe, 'OPTIONAL_HEADER') or not hasattr(pe.OPTIONAL_HEADER, 'DATA_DIRECTORY'):
|
||||
return res
|
||||
|
||||
sec_idx = 4
|
||||
directories = pe.OPTIONAL_HEADER.DATA_DIRECTORY
|
||||
if len(directories) <= sec_idx:
|
||||
return res
|
||||
|
||||
sec_dir = directories[sec_idx]
|
||||
if sec_dir.Size == 0 or sec_dir.VirtualAddress == 0:
|
||||
return res
|
||||
|
||||
res["HasSignature"] = 1.0
|
||||
offset = sec_dir.VirtualAddress
|
||||
size = sec_dir.Size
|
||||
|
||||
if offset + size > fsize or offset < 0 or size < 0:
|
||||
return res
|
||||
|
||||
count = 0
|
||||
curr = offset
|
||||
end = offset + size
|
||||
|
||||
while curr + 8 <= end:
|
||||
dw_length = int.from_bytes(file_bytes[curr:curr+4], 'little')
|
||||
if dw_length < 8:
|
||||
break
|
||||
count += 1
|
||||
curr += (dw_length + 7) & ~7
|
||||
|
||||
res["SignatureCount"] = float(count)
|
||||
return res
|
||||
|
||||
def extract_features(self, file_path):
|
||||
pe = None
|
||||
try:
|
||||
fsize = os.path.getsize(file_path)
|
||||
if fsize == 0 or fsize > 536870912:
|
||||
return None
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
base = {}
|
||||
dlls = set()
|
||||
apis = set()
|
||||
pe = pefile.PE(data=file_bytes, fast_load=True)
|
||||
|
||||
try:
|
||||
pe.parse_rich_header()
|
||||
pe.parse_data_directories(directories=[
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_DEBUG'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_TLS'],
|
||||
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']
|
||||
])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base['TrustSigned'] = 1.0 if self.signer.sign_verify(file_path) else 0.0
|
||||
base['FileEntropy'] = self._calc_entropy(file_bytes)
|
||||
base['FileSize'] = float(fsize)
|
||||
|
||||
fh = pe.FILE_HEADER
|
||||
base['Machine'] = getattr(fh, 'Machine', 0)
|
||||
base['NumberOfSections'] = getattr(fh, 'NumberOfSections', 0)
|
||||
base['TimeDateStamp'] = getattr(fh, 'TimeDateStamp', 0)
|
||||
base['PointerToSymbolTable'] = getattr(fh, 'PointerToSymbolTable', 0)
|
||||
base['NumberOfSymbols'] = getattr(fh, 'NumberOfSymbols', 0)
|
||||
base['SizeOfOptionalHeader'] = getattr(fh, 'SizeOfOptionalHeader', 0)
|
||||
base['Characteristics'] = getattr(fh, 'Characteristics', 0)
|
||||
|
||||
curr_ts = datetime.datetime.utcnow().timestamp()
|
||||
base['HasInvalidTimestamp'] = 1.0 if (base['TimeDateStamp'] < 631152000 or base['TimeDateStamp'] > curr_ts + 2592000) else 0.0
|
||||
base['FileTimeException'] = 1.0 if base['TimeDateStamp'] == 0 else 0.0
|
||||
base['Is64Bit'] = 1.0 if base['Machine'] in (0x8664, 0xAA64, 0x0200) else 0.0
|
||||
base['IsExe'] = 1.0 if pe.is_exe() else 0.0
|
||||
base['IsDll'] = 1.0 if pe.is_dll() else 0.0
|
||||
|
||||
base['ExceptionCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'OPTIONAL_HEADER'):
|
||||
op = pe.OPTIONAL_HEADER
|
||||
fields = [
|
||||
'Magic', 'MajorLinkerVersion', 'MinorLinkerVersion', 'SizeOfCode',
|
||||
'SizeOfInitializedData', 'SizeOfUninitializedData', 'AddressOfEntryPoint',
|
||||
'BaseOfCode', 'ImageBase', 'SectionAlignment', 'FileAlignment',
|
||||
'MajorOperatingSystemVersion', 'MinorOperatingSystemVersion',
|
||||
'MajorImageVersion', 'MinorImageVersion', 'MajorSubsystemVersion',
|
||||
'MinorSubsystemVersion', 'SizeOfImage', 'SizeOfHeaders', 'CheckSum',
|
||||
'Subsystem', 'DllCharacteristics', 'SizeOfStackReserve', 'SizeOfStackCommit',
|
||||
'SizeOfHeapReserve', 'SizeOfHeapCommit', 'LoaderFlags', 'NumberOfRvaAndSizes'
|
||||
]
|
||||
for f_field in fields:
|
||||
base[f_field] = getattr(op, f_field, 0)
|
||||
|
||||
base['IsDriver'] = 1.0 if base.get('Subsystem') == 1 else 0.0
|
||||
|
||||
if hasattr(op, 'DATA_DIRECTORY'):
|
||||
for i, directory in enumerate(op.DATA_DIRECTORY):
|
||||
base[f'DataDirectory_{i}_Size'] = directory.Size
|
||||
base[f'DataDirectory_{i}_VA'] = directory.VirtualAddress
|
||||
|
||||
exc_idx = pefile.DIRECTORY_ENTRY.get('IMAGE_DIRECTORY_ENTRY_EXCEPTION', 3)
|
||||
if len(op.DATA_DIRECTORY) > exc_idx and op.DATA_DIRECTORY[exc_idx].Size > 0:
|
||||
base['ExceptionCount'] = op.DATA_DIRECTORY[exc_idx].Size // 12 if base['Machine'] in (0x8664, 0xAA64) else 0.0
|
||||
|
||||
res_flags_count = dict.fromkeys(self._CHAR_COUNT_KEYS.values(), 0.0)
|
||||
res_flags_ent = dict.fromkeys(self._CHAR_ENT_KEYS.values(), 0.0)
|
||||
res_sec_hash = dict.fromkeys(self._SECTION_HASH_KEYS, 0.0)
|
||||
|
||||
if hasattr(pe, 'sections'):
|
||||
base['SectionCount'] = len(pe.sections)
|
||||
entropies = []
|
||||
raw_sizes = []
|
||||
v_sizes = []
|
||||
exec_sec = write_sec = read_sec = sec_exc = 0
|
||||
|
||||
for section in pe.sections:
|
||||
sec_name = section.Name.rstrip(b'\x00')
|
||||
hash_val = zlib.crc32(sec_name) % 50
|
||||
res_sec_hash[self._SECTION_HASH_KEYS[hash_val]] += 1.0
|
||||
|
||||
try:
|
||||
s_data = section.get_data()
|
||||
s_entropy = self._calc_entropy(s_data)
|
||||
except Exception:
|
||||
s_entropy = 0.0
|
||||
|
||||
entropies.append(s_entropy)
|
||||
raw_sizes.append(section.SizeOfRawData)
|
||||
v_sizes.append(section.Misc_VirtualSize)
|
||||
|
||||
if section.Characteristics & 0x20000000: exec_sec += 1
|
||||
if section.Characteristics & 0x80000000: write_sec += 1
|
||||
if section.Characteristics & 0x40000000: read_sec += 1
|
||||
if section.SizeOfRawData + section.PointerToRawData > fsize: sec_exc = 1
|
||||
|
||||
for flag in self._CHAR_FLAGS:
|
||||
if section.Characteristics & flag:
|
||||
res_flags_count[self._CHAR_COUNT_KEYS[flag]] += 1.0
|
||||
res_flags_ent[self._CHAR_ENT_KEYS[flag]] += s_entropy
|
||||
|
||||
base['SectionMaxEntropy'] = max(entropies) if entropies else 0.0
|
||||
base['SectionMinEntropy'] = min(entropies) if entropies else 0.0
|
||||
base['SectionMeanEntropy'] = sum(entropies) / len(entropies) if entropies else 0.0
|
||||
base['SectionMaxRawSize'] = max(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMinRawSize'] = min(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMeanRawSize'] = sum(raw_sizes) / len(raw_sizes) if raw_sizes else 0.0
|
||||
base['SectionMaxVSize'] = max(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMinVSize'] = min(v_sizes) if v_sizes else 0.0
|
||||
base['SectionMeanVSize'] = sum(v_sizes) / len(v_sizes) if v_sizes else 0.0
|
||||
base['ExecutableSections'] = float(exec_sec)
|
||||
base['WritableSections'] = float(write_sec)
|
||||
base['ReadableSections'] = float(read_sec)
|
||||
base['SectionException'] = float(sec_exc)
|
||||
|
||||
for flag in self._CHAR_FLAGS:
|
||||
cnt_key = self._CHAR_COUNT_KEYS[flag]
|
||||
if res_flags_count[cnt_key] > 0:
|
||||
res_flags_ent[self._CHAR_ENT_KEYS[flag]] /= res_flags_count[cnt_key]
|
||||
|
||||
base.update(res_flags_count)
|
||||
base.update(res_flags_ent)
|
||||
base.update(res_sec_hash)
|
||||
|
||||
string_keys = ['FileDescription', 'FileVersion', 'ProductName', 'ProductVersion', 'CompanyName', 'LegalCopyright', 'Comments', 'InternalName', 'LegalTrademarks', 'SpecialBuild', 'PrivateBuild']
|
||||
for key in string_keys:
|
||||
base[f'{key}Length'] = 0.0
|
||||
|
||||
if hasattr(pe, 'FileInfo'):
|
||||
for fileinfo_list in pe.FileInfo:
|
||||
for fileinfo in fileinfo_list:
|
||||
if getattr(fileinfo, 'name', '') in ('StringFileInfo', b'StringFileInfo'):
|
||||
for st in getattr(fileinfo, 'StringTable', []):
|
||||
for key, val in st.entries.items():
|
||||
try:
|
||||
k = key.decode('utf-8', 'ignore') if isinstance(key, bytes) else str(key)
|
||||
if k in string_keys:
|
||||
v = val.decode('utf-8', 'ignore') if isinstance(val, bytes) else str(val)
|
||||
base[f'{k}Length'] = float(len(v))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_TLS') and hasattr(pe.DIRECTORY_ENTRY_TLS, 'struct'):
|
||||
if getattr(pe.DIRECTORY_ENTRY_TLS.struct, 'AddressOfCallBacks', 0) != 0:
|
||||
base['HasTlsCallbacks'] = 1.0
|
||||
|
||||
if hasattr(pe, 'VS_FIXEDFILEINFO') and len(pe.VS_FIXEDFILEINFO) > 0:
|
||||
flags = getattr(pe.VS_FIXEDFILEINFO[0], 'FileFlags', 0)
|
||||
base['IsDebug'] = 1.0 if flags & 0x1 else 0.0
|
||||
base['IsPreRelease'] = 1.0 if flags & 0x2 else 0.0
|
||||
base['IsPatched'] = 1.0 if flags & 0x4 else 0.0
|
||||
base['IsPrivateBuild'] = 1.0 if flags & 0x8 else 0.0
|
||||
base['IsSpecialBuild'] = 1.0 if flags & 0x20 else 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
|
||||
base['ImportCount'] = float(len(pe.DIRECTORY_ENTRY_IMPORT))
|
||||
func_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
||||
if getattr(entry, 'dll', None):
|
||||
try:
|
||||
dlls.add(entry.dll.decode('ascii', 'ignore').lower())
|
||||
except Exception:
|
||||
pass
|
||||
for imp in getattr(entry, 'imports', []):
|
||||
func_count += 1
|
||||
if getattr(imp, 'name', None):
|
||||
try:
|
||||
apis.add(imp.name.decode('ascii', 'ignore'))
|
||||
except Exception:
|
||||
pass
|
||||
base['ImportFunctionCount'] = float(func_count)
|
||||
else:
|
||||
base['ImportCount'] = 0.0
|
||||
base['ImportFunctionCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT') and hasattr(pe.DIRECTORY_ENTRY_EXPORT, 'symbols'):
|
||||
base['ExportCount'] = float(len(pe.DIRECTORY_ENTRY_EXPORT.symbols))
|
||||
else:
|
||||
base['ExportCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
||||
icon_count = 0
|
||||
for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
if getattr(entry, 'id', None) == 3 and hasattr(entry, 'directory'):
|
||||
icon_count += len(getattr(entry.directory, 'entries', []))
|
||||
base['IconCount'] = float(icon_count)
|
||||
else:
|
||||
base['IconCount'] = 0.0
|
||||
|
||||
if hasattr(pe, 'DIRECTORY_ENTRY_DEBUG'):
|
||||
base['DebugCount'] = float(len(pe.DIRECTORY_ENTRY_DEBUG))
|
||||
else:
|
||||
base['DebugCount'] = 0.0
|
||||
|
||||
base.update(self._extract_strings(file_bytes))
|
||||
base.update(self._extract_histograms(file_bytes))
|
||||
base.update(self._extract_overlay_features(pe, fsize))
|
||||
base.update(self._extract_rich_header(pe))
|
||||
base.update(self._extract_ep_anomalies(pe))
|
||||
base.update(self._extract_advanced_resources(pe))
|
||||
base.update(self._extract_load_config(pe))
|
||||
base.update(self._extract_security_directory(pe, file_bytes, fsize))
|
||||
|
||||
for k in base:
|
||||
base[k] = self._safe_float(base[k])
|
||||
|
||||
return {"Base": base, "DLLs": list(dlls), "APIs": list(apis)}
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
if pe:
|
||||
pe.close()
|
||||
|
||||
def pe_scan(self, file_path, enhanced_mode=False):
|
||||
if not self.model or not self.feature_order:
|
||||
return False, False
|
||||
try:
|
||||
raw_data = self.extract_features(file_path)
|
||||
if not raw_data:
|
||||
return False, False
|
||||
|
||||
vec = numpy.zeros((1, len(self.feature_order)), dtype=numpy.float32)
|
||||
base = raw_data.get('Base', {})
|
||||
dlls = set(raw_data.get('DLLs', []))
|
||||
apis = set(raw_data.get('APIs', []))
|
||||
feat_map = {feat: i for i, feat in enumerate(self.feature_order)}
|
||||
|
||||
for k, v in base.items():
|
||||
if k in feat_map:
|
||||
vec[0, feat_map[k]] = v
|
||||
|
||||
for d in dlls:
|
||||
old_fn = f"Dll_{d}"
|
||||
if old_fn in feat_map:
|
||||
vec[0, feat_map[old_fn]] = 1.0
|
||||
h = zlib.crc32(d.encode('utf-8', 'ignore')) % 256
|
||||
new_fn = f"DllHash_{h:03d}"
|
||||
if new_fn in feat_map:
|
||||
vec[0, feat_map[new_fn]] += 1.0
|
||||
|
||||
for a in apis:
|
||||
old_fn = f"Api_{a}"
|
||||
if old_fn in feat_map:
|
||||
vec[0, feat_map[old_fn]] = 1.0
|
||||
h = zlib.crc32(a.encode('utf-8', 'ignore')) % 1024
|
||||
new_fn = f"ApiHash_{h:04d}"
|
||||
if new_fn in feat_map:
|
||||
vec[0, feat_map[new_fn]] += 1.0
|
||||
|
||||
outputs = self.model.run(None, {self.input_name: vec})
|
||||
result = outputs[1]
|
||||
prob = 0.0
|
||||
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
prob_dict = result[0]
|
||||
if hasattr(prob_dict, 'get'):
|
||||
prob = float(prob_dict.get(1, prob_dict.get('1', 0.0)))
|
||||
elif isinstance(result, numpy.ndarray):
|
||||
if result.ndim == 2 and result.shape[1] > 1:
|
||||
prob = float(result[0][1])
|
||||
|
||||
score = int(prob * 100)
|
||||
|
||||
if score >= 80:
|
||||
return f"General:WinPE/Malware.{score}!ml", score
|
||||
elif enhanced_mode and score >= 50:
|
||||
return f"General:WinPE/Suspicious.{score}!ml", score
|
||||
|
||||
return False, False
|
||||
except Exception:
|
||||
return False, False
|
||||
|
||||
####################################################################################################
|
||||
|
||||
class cloud_scanner:
|
||||
def __init__(self):
|
||||
pass
|
||||
self.session = None
|
||||
self.api_host = None
|
||||
self.api_key = None
|
||||
self.timeout = 30
|
||||
self.lock = threading.RLock()
|
||||
|
||||
def _init_session(self, api_host, api_key):
|
||||
with self.lock:
|
||||
if self.session is None or self.api_host != api_host or self.api_key != api_key:
|
||||
self.api_host = api_host.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"X-API-Key": api_key, "User-Agent": "PYAS-Engine/1.1"})
|
||||
|
||||
def _request(self, method, endpoint, **kwargs):
|
||||
with self.lock:
|
||||
current_session = self.session
|
||||
current_host = self.api_host
|
||||
|
||||
if not current_session:
|
||||
return None
|
||||
|
||||
try:
|
||||
r = current_session.request(method, f"{current_host}{endpoint}", timeout=self.timeout, **kwargs)
|
||||
if r.status_code == 200:
|
||||
return r
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def calc_hash(self, file_path, block_size=65536):
|
||||
try:
|
||||
@@ -246,64 +789,97 @@ class cloud_scanner:
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def upload_file(self, file_path, api_host, api_key):
|
||||
####################################################################################################
|
||||
|
||||
def rescan(self, sha256, api_host, api_key):
|
||||
self._init_session(api_host, api_key)
|
||||
r = self._request("POST", f"/api/rescan/{sha256}")
|
||||
return r is not None and r.json().get('status') == 'success'
|
||||
|
||||
def upload_file(self, file_path, api_host, api_key, chunk_size=4194304, need_rescan=False, max_retries=3):
|
||||
try:
|
||||
self._init_session(api_host, api_key)
|
||||
sha256 = self.calc_hash(file_path)
|
||||
if not sha256:
|
||||
return False, None
|
||||
|
||||
base = api_host.rstrip('/')
|
||||
base_headers = {"X-API-Key": api_key, "User-Agent": "PYAS-Client/1.0"}
|
||||
status_req = self._request("GET", f"/api/processing_status/{sha256}")
|
||||
if status_req:
|
||||
current_status = status_req.json().get('status')
|
||||
if current_status == 'done':
|
||||
if need_rescan:
|
||||
self.rescan(sha256, api_host, api_key)
|
||||
return True, sha256
|
||||
elif current_status in ('queued', 'processing'):
|
||||
return True, sha256
|
||||
|
||||
def _req(m, ep, files=None, **k):
|
||||
try:
|
||||
return requests.request(m, f"{base}{ep}", headers=base_headers, files=files, timeout=120, **k)
|
||||
except:
|
||||
return None
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > 104857600:
|
||||
return False, sha256
|
||||
|
||||
r = _req("GET", f"/api/processing_status/{sha256}")
|
||||
status = r.json().get('status') if r and r.status_code == 200 else 'missing'
|
||||
if status in ['failed', 'error']:
|
||||
status = 'missing'
|
||||
|
||||
if status == 'missing':
|
||||
with open(file_path, 'rb') as f:
|
||||
r = _req("POST", "/api/upload", files={'file': f})
|
||||
if not r or r.status_code != 200:
|
||||
total_chunks = max(1, (file_size + chunk_size - 1) // chunk_size)
|
||||
upload_id = os.urandom(16).hex()
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
for i in range(total_chunks):
|
||||
chunk_data = f.read(chunk_size)
|
||||
headers = {
|
||||
"X-Chunk-Index": str(i),
|
||||
"X-Total-Chunks": str(total_chunks),
|
||||
"X-Upload-ID": upload_id
|
||||
}
|
||||
|
||||
chunk_success = False
|
||||
for attempt in range(max_retries):
|
||||
r = self._request("POST", "/api/upload", files={'file': (os.path.basename(file_path), chunk_data)}, headers=headers)
|
||||
if r:
|
||||
if i == total_chunks - 1:
|
||||
try:
|
||||
resp = r.json()
|
||||
if 'url' in resp:
|
||||
sha256 = resp.get('url', '').split('/')[-1]
|
||||
except Exception:
|
||||
pass
|
||||
chunk_success = True
|
||||
break
|
||||
time.sleep(2 ** attempt)
|
||||
|
||||
if not chunk_success:
|
||||
return False, sha256
|
||||
|
||||
|
||||
return True, sha256
|
||||
except Exception:
|
||||
return False, None
|
||||
|
||||
def get_result(self, sha256, api_host, api_key):
|
||||
####################################################################################################
|
||||
|
||||
def get_result(self, sha256, api_host, api_key, max_retries=6, interval=10):
|
||||
try:
|
||||
if not sha256:
|
||||
return False
|
||||
self._init_session(api_host, api_key)
|
||||
|
||||
base = api_host.rstrip('/')
|
||||
headers = {"X-API-Key": api_key, "User-Agent": "PYAS-Client/1.0"}
|
||||
is_done = False
|
||||
for _ in range(max_retries):
|
||||
r = self._request("GET", f"/api/processing_status/{sha256}")
|
||||
if r:
|
||||
st = r.json().get('status', 'error')
|
||||
if st == 'done':
|
||||
is_done = True
|
||||
break
|
||||
if st in ['error', 'failed']:
|
||||
return False
|
||||
time.sleep(interval)
|
||||
|
||||
def _req(m, ep, **k):
|
||||
try:
|
||||
return requests.request(m, f"{base}{ep}", headers=headers, timeout=10, **k)
|
||||
except:
|
||||
return None
|
||||
if not is_done:
|
||||
return False
|
||||
|
||||
for _ in range(30):
|
||||
r = _req("GET", f"/api/processing_status/{sha256}")
|
||||
st = r.json().get('status') if r else 'error'
|
||||
if st == 'done':
|
||||
break
|
||||
if st in ['error', 'failed']:
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
||||
r = _req("GET", f"/api/report/{sha256}")
|
||||
if r and r.status_code == 200:
|
||||
r = self._request("GET", f"/api/report/{sha256}")
|
||||
if r:
|
||||
data = r.json().get('data', {})
|
||||
res = data.get('detection', {}).get('results', {}).get('PYAS', {})
|
||||
label = res.get('label', 'Unsupport')
|
||||
metadata = data.get('metadata', {})
|
||||
label = metadata.get('label', 'Unsupport')
|
||||
score = metadata.get('score', 0)
|
||||
sims = data.get('similar', [])
|
||||
|
||||
is_malicious = 'General' in label
|
||||
@@ -312,11 +888,11 @@ class cloud_scanner:
|
||||
for s in sims:
|
||||
if s.get('similarity', 0) > 80:
|
||||
valid_sim_count += 1
|
||||
if "General" in s.get('label'):
|
||||
if "General" in s.get('label', ''):
|
||||
sim_malicious_count += 1
|
||||
|
||||
if is_malicious and (valid_sim_count == 0 or sim_malicious_count == valid_sim_count):
|
||||
return True
|
||||
return f"General:WinPE/Malware.{score}!cl"
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
-1576
File diff suppressed because it is too large.
Load diff
-14520
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -2,9 +2,9 @@ import pyinstaller_versionfile
|
||||
|
||||
pyinstaller_versionfile.create_versionfile(
|
||||
output_file="versionfile.txt",
|
||||
version='3.3.8',
|
||||
version='3.5.7.0',
|
||||
company_name="PYAS Security",
|
||||
file_description="Python Antivirus Software",
|
||||
file_description="PYAS Security Antivirus",
|
||||
internal_name="PYAS",
|
||||
legal_copyright="PYAS Security",
|
||||
original_filename="PYAS.exe",
|
||||
|
||||
@@ -43,11 +43,25 @@ constexpr auto PYAS_POOL_TAG = 'SAYP';
|
||||
#define PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
|
||||
#endif
|
||||
#ifndef PROCESS_SET_QUOTA
|
||||
#define PROCESS_SET_QUOTA (0x0100)
|
||||
#define PROCESS_SET_QUOTA (0x0100)
|
||||
#endif
|
||||
#ifndef THREAD_TERMINATE
|
||||
#define THREAD_TERMINATE (0x0001)
|
||||
#endif
|
||||
#ifndef THREAD_SUSPEND_RESUME
|
||||
#define THREAD_SUSPEND_RESUME (0x0002)
|
||||
#endif
|
||||
#ifndef THREAD_SET_CONTEXT
|
||||
#define THREAD_SET_CONTEXT (0x0010)
|
||||
#endif
|
||||
#ifndef THREAD_SET_INFORMATION
|
||||
#define THREAD_SET_INFORMATION (0x0020)
|
||||
#endif
|
||||
#ifndef THREAD_SET_THREAD_TOKEN
|
||||
#define THREAD_SET_THREAD_TOKEN (0x0080)
|
||||
#endif
|
||||
|
||||
#ifndef POOL_FLAG_NON_PAGED
|
||||
#define POOL_FLAG_NON_PAGED 0x0000000000000040UI64
|
||||
#define POOL_FLAG_NON_PAGED 0x0000000000000040UI64
|
||||
#endif
|
||||
|
||||
#ifndef FSCTL_MANAGE_BYPASS_IO
|
||||
@@ -87,6 +101,11 @@ typedef struct _PYAS_MESSAGE {
|
||||
WCHAR Path[MAX_PATH_LEN];
|
||||
} PYAS_MESSAGE, * PPYAS_MESSAGE;
|
||||
|
||||
typedef struct _PYAS_USER_MESSAGE {
|
||||
ULONG Command;
|
||||
WCHAR Path[MAX_PATH_LEN];
|
||||
} PYAS_USER_MESSAGE, * PPYAS_USER_MESSAGE;
|
||||
|
||||
typedef struct _DRIVER_DATA {
|
||||
PDRIVER_OBJECT DriverObject;
|
||||
PFLT_FILTER FilterHandle;
|
||||
@@ -95,17 +114,23 @@ typedef struct _DRIVER_DATA {
|
||||
PEPROCESS PyasProcess;
|
||||
ULONG PyasPid;
|
||||
LARGE_INTEGER Cookie;
|
||||
FAST_MUTEX PortMutex;
|
||||
FAST_MUTEX TrackerMutex;
|
||||
KSPIN_LOCK PortMutex;
|
||||
KSPIN_LOCK TrackerMutex;
|
||||
EX_RUNDOWN_REF PortRundown;
|
||||
} DRIVER_DATA, * PDRIVER_DATA;
|
||||
|
||||
typedef struct _RULE_NODE {
|
||||
struct _RULE_NODE* Next;
|
||||
UNICODE_STRING Pattern;
|
||||
} RULE_NODE, * PRULE_NODE;
|
||||
|
||||
extern DRIVER_DATA GlobalData;
|
||||
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_PreWrite(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_PreSetInfo(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_FileSystemControl(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_SetSecurity(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectBoot_PreDeviceControl(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext);
|
||||
|
||||
NTSTATUS InitializeProcessProtection();
|
||||
@@ -113,28 +138,32 @@ VOID UninitializeProcessProtection();
|
||||
NTSTATUS InitializeRegistryProtection(PDRIVER_OBJECT DriverObject);
|
||||
VOID UninitializeRegistryProtection();
|
||||
|
||||
NTSTATUS LoadRulesFromDisk(PUNICODE_STRING RegistryPath);
|
||||
VOID UnloadRules();
|
||||
|
||||
VOID InitializeRulesEngine();
|
||||
VOID UninitializeRulesEngine();
|
||||
|
||||
VOID AddDynamicWhitelist(PUNICODE_STRING RuleStr);
|
||||
VOID RemoveDynamicWhitelist(PUNICODE_STRING RuleStr);
|
||||
|
||||
VOID ImageLoadNotify(PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo);
|
||||
|
||||
NTSTATUS GetProcessImageName(HANDLE ProcessId, PUNICODE_STRING* ImageName);
|
||||
|
||||
BOOLEAN IsProcessTrusted(HANDLE ProcessId);
|
||||
BOOLEAN IsTargetProtected(HANDLE ProcessId);
|
||||
BOOLEAN IsInstallerProcess(HANDLE ProcessId);
|
||||
BOOLEAN WildcardMatch(PCWSTR Pattern, PCWSTR String, USHORT StringLengthBytes);
|
||||
|
||||
BOOLEAN CheckRegistryRule(PCUNICODE_STRING KeyName);
|
||||
BOOLEAN CheckFileExtensionRule(PCUNICODE_STRING FileName);
|
||||
BOOLEAN CheckProtectedPathRule(PCUNICODE_STRING FileName);
|
||||
BOOLEAN CheckRansomActivity(HANDLE ProcessId, PUNICODE_STRING FileName, PVOID Buffer, ULONG Length, BOOLEAN IsWrite);
|
||||
BOOLEAN CheckRansomActivity(HANDLE ProcessId, PUNICODE_STRING FileName, PVOID WriteBuffer, ULONG WriteLength, BOOLEAN IsWrite);
|
||||
|
||||
NTSTATUS SendMessageToUser(ULONG Code, ULONG Pid, PWCHAR Path, USHORT PathSize);
|
||||
|
||||
FORCEINLINE PVOID PyasAllocate(SIZE_T Size) {
|
||||
#if (NTDDI_VERSION >= NTDDI_WIN10_VB)
|
||||
return ExAllocatePool2(POOL_FLAG_NON_PAGED, Size, PYAS_POOL_TAG);
|
||||
#else
|
||||
PVOID Buffer = ExAllocatePoolWithTag(NonPagedPoolNx, Size, PYAS_POOL_TAG);
|
||||
if (Buffer) RtlZeroMemory(Buffer, Size);
|
||||
return Buffer;
|
||||
#endif
|
||||
}
|
||||
|
||||
FORCEINLINE VOID PyasFree(PVOID Ptr) {
|
||||
|
||||
+101
-22
@@ -1,6 +1,7 @@
|
||||
#include "DriverCommon.h"
|
||||
|
||||
DRIVER_DATA GlobalData;
|
||||
static BOOLEAN g_ImageNotifyRegistered = FALSE;
|
||||
|
||||
static NTSTATUS InstanceSetup(PCFLT_RELATED_OBJECTS FltObjects, FLT_INSTANCE_SETUP_FLAGS Flags, DEVICE_TYPE VolumeDeviceType, FLT_FILESYSTEM_TYPE VolumeFilesystemType) {
|
||||
UNREFERENCED_PARAMETER(FltObjects);
|
||||
@@ -29,17 +30,54 @@ static VOID InstanceTeardownComplete(PCFLT_RELATED_OBJECTS FltObjects, FLT_INSTA
|
||||
static NTSTATUS DriverUnload(FLT_FILTER_UNLOAD_FLAGS Flags) {
|
||||
UNREFERENCED_PARAMETER(Flags);
|
||||
|
||||
PsRemoveLoadImageNotifyRoutine(ImageLoadNotify);
|
||||
if (GlobalData.ServerPort) {
|
||||
FltCloseCommunicationPort(GlobalData.ServerPort);
|
||||
GlobalData.ServerPort = NULL;
|
||||
}
|
||||
|
||||
if (GlobalData.FilterHandle) {
|
||||
FltUnregisterFilter(GlobalData.FilterHandle);
|
||||
GlobalData.FilterHandle = NULL;
|
||||
}
|
||||
|
||||
if (g_ImageNotifyRegistered) {
|
||||
PsRemoveLoadImageNotifyRoutine(ImageLoadNotify);
|
||||
g_ImageNotifyRegistered = FALSE;
|
||||
}
|
||||
|
||||
UninitializeRegistryProtection();
|
||||
UninitializeProcessProtection();
|
||||
|
||||
ExWaitForRundownProtectionRelease(&GlobalData.PortRundown);
|
||||
UnloadRules();
|
||||
UninitializeRulesEngine();
|
||||
|
||||
if (GlobalData.ServerPort) {
|
||||
FltCloseCommunicationPort(GlobalData.ServerPort);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static NTSTATUS PortMessage(PVOID PortCookie, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength, PULONG ReturnOutputBufferLength) {
|
||||
UNREFERENCED_PARAMETER(PortCookie);
|
||||
UNREFERENCED_PARAMETER(OutputBuffer);
|
||||
UNREFERENCED_PARAMETER(OutputBufferLength);
|
||||
|
||||
if (InputBuffer && InputBufferLength >= sizeof(PYAS_USER_MESSAGE)) {
|
||||
PPYAS_USER_MESSAGE msg = (PPYAS_USER_MESSAGE)InputBuffer;
|
||||
if (msg->Command == 1 || msg->Command == 2) {
|
||||
msg->Path[MAX_PATH_LEN - 1] = L'\0';
|
||||
UNICODE_STRING us;
|
||||
RtlInitUnicodeString(&us, msg->Path);
|
||||
if (us.Length > 0) {
|
||||
if (msg->Command == 1) {
|
||||
AddDynamicWhitelist(&us);
|
||||
}
|
||||
else {
|
||||
RemoveDynamicWhitelist(&us);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GlobalData.FilterHandle) {
|
||||
FltUnregisterFilter(GlobalData.FilterHandle);
|
||||
|
||||
if (ReturnOutputBufferLength) {
|
||||
*ReturnOutputBufferLength = 0;
|
||||
}
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
@@ -50,7 +88,8 @@ static NTSTATUS PortConnect(PFLT_PORT ClientPort, PVOID ServerPortCookie, PVOID
|
||||
UNREFERENCED_PARAMETER(SizeOfContext);
|
||||
UNREFERENCED_PARAMETER(ConnectionPortCookie);
|
||||
|
||||
ExAcquireFastMutex(&GlobalData.PortMutex);
|
||||
KIRQL OldIrql;
|
||||
KeAcquireSpinLock(&GlobalData.PortMutex, &OldIrql);
|
||||
|
||||
GlobalData.ClientPort = ClientPort;
|
||||
GlobalData.PyasPid = (ULONG)(ULONG_PTR)PsGetCurrentProcessId();
|
||||
@@ -58,7 +97,7 @@ static NTSTATUS PortConnect(PFLT_PORT ClientPort, PVOID ServerPortCookie, PVOID
|
||||
|
||||
ExReInitializeRundownProtection(&GlobalData.PortRundown);
|
||||
|
||||
ExReleaseFastMutex(&GlobalData.PortMutex);
|
||||
KeReleaseSpinLock(&GlobalData.PortMutex, OldIrql);
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
@@ -66,13 +105,14 @@ static NTSTATUS PortConnect(PFLT_PORT ClientPort, PVOID ServerPortCookie, PVOID
|
||||
static VOID PortDisconnect(PVOID ConnectionCookie) {
|
||||
UNREFERENCED_PARAMETER(ConnectionCookie);
|
||||
|
||||
ExAcquireFastMutex(&GlobalData.PortMutex);
|
||||
KIRQL OldIrql;
|
||||
KeAcquireSpinLock(&GlobalData.PortMutex, &OldIrql);
|
||||
|
||||
GlobalData.ClientPort = NULL;
|
||||
GlobalData.PyasPid = 0;
|
||||
if (GlobalData.PyasProcess) {
|
||||
GlobalData.PyasProcess = NULL;
|
||||
}
|
||||
ExReleaseFastMutex(&GlobalData.PortMutex);
|
||||
GlobalData.PyasProcess = NULL;
|
||||
|
||||
KeReleaseSpinLock(&GlobalData.PortMutex, OldIrql);
|
||||
|
||||
ExWaitForRundownProtectionRelease(&GlobalData.PortRundown);
|
||||
}
|
||||
@@ -81,6 +121,7 @@ CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
|
||||
{ IRP_MJ_CREATE, 0, ProtectFile_PreCreate, NULL },
|
||||
{ IRP_MJ_WRITE, 0, ProtectFile_PreWrite, NULL },
|
||||
{ IRP_MJ_SET_INFORMATION, 0, ProtectFile_PreSetInfo, NULL },
|
||||
{ IRP_MJ_SET_SECURITY, 0, ProtectFile_SetSecurity, NULL },
|
||||
{ IRP_MJ_FILE_SYSTEM_CONTROL, 0, ProtectFile_FileSystemControl, NULL },
|
||||
{ IRP_MJ_DEVICE_CONTROL, 0, ProtectBoot_PreDeviceControl, NULL },
|
||||
{ IRP_MJ_OPERATION_END }
|
||||
@@ -103,8 +144,6 @@ CONST FLT_REGISTRATION FilterRegistration = {
|
||||
};
|
||||
|
||||
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
|
||||
UNREFERENCED_PARAMETER(RegistryPath);
|
||||
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
PSECURITY_DESCRIPTOR sd = NULL;
|
||||
OBJECT_ATTRIBUTES oa = { 0 };
|
||||
@@ -112,30 +151,70 @@ extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING Reg
|
||||
|
||||
RtlZeroMemory(&GlobalData, sizeof(GlobalData));
|
||||
GlobalData.DriverObject = DriverObject;
|
||||
ExInitializeFastMutex(&GlobalData.PortMutex);
|
||||
ExInitializeFastMutex(&GlobalData.TrackerMutex);
|
||||
KeInitializeSpinLock(&GlobalData.PortMutex);
|
||||
KeInitializeSpinLock(&GlobalData.TrackerMutex);
|
||||
|
||||
ExInitializeRundownProtection(&GlobalData.PortRundown);
|
||||
|
||||
InitializeRulesEngine();
|
||||
|
||||
status = LoadRulesFromDisk(RegistryPath);
|
||||
|
||||
status = FltRegisterFilter(DriverObject, &FilterRegistration, &GlobalData.FilterHandle);
|
||||
if (!NT_SUCCESS(status)) return status;
|
||||
if (!NT_SUCCESS(status)) {
|
||||
UnloadRules();
|
||||
UninitializeRulesEngine();
|
||||
return status;
|
||||
}
|
||||
|
||||
status = FltBuildDefaultSecurityDescriptor(&sd, FLT_PORT_ALL_ACCESS);
|
||||
if (NT_SUCCESS(status)) {
|
||||
RtlInitUnicodeString(&name, PYAS_PORT_NAME);
|
||||
InitializeObjectAttributes(&oa, &name, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, sd);
|
||||
status = FltCreateCommunicationPort(GlobalData.FilterHandle, &GlobalData.ServerPort, &oa, NULL, PortConnect, PortDisconnect, NULL, 1);
|
||||
status = FltCreateCommunicationPort(GlobalData.FilterHandle, &GlobalData.ServerPort, &oa, NULL, PortConnect, PortDisconnect, PortMessage, 1);
|
||||
FltFreeSecurityDescriptor(sd);
|
||||
}
|
||||
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FltUnregisterFilter(GlobalData.FilterHandle);
|
||||
GlobalData.FilterHandle = NULL;
|
||||
UnloadRules();
|
||||
UninitializeRulesEngine();
|
||||
return status;
|
||||
}
|
||||
|
||||
InitializeProcessProtection();
|
||||
InitializeRegistryProtection(DriverObject);
|
||||
PsSetLoadImageNotifyRoutine(ImageLoadNotify);
|
||||
|
||||
return FltStartFiltering(GlobalData.FilterHandle);
|
||||
InitializeRegistryProtection(DriverObject);
|
||||
|
||||
NTSTATUS imageNotifyStatus = PsSetLoadImageNotifyRoutine(ImageLoadNotify);
|
||||
if (NT_SUCCESS(imageNotifyStatus)) {
|
||||
g_ImageNotifyRegistered = TRUE;
|
||||
}
|
||||
|
||||
status = FltStartFiltering(GlobalData.FilterHandle);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
if (g_ImageNotifyRegistered) {
|
||||
PsRemoveLoadImageNotifyRoutine(ImageLoadNotify);
|
||||
g_ImageNotifyRegistered = FALSE;
|
||||
}
|
||||
|
||||
UninitializeRegistryProtection();
|
||||
UninitializeProcessProtection();
|
||||
|
||||
if (GlobalData.ServerPort) {
|
||||
FltCloseCommunicationPort(GlobalData.ServerPort);
|
||||
GlobalData.ServerPort = NULL;
|
||||
}
|
||||
|
||||
if (GlobalData.FilterHandle) {
|
||||
FltUnregisterFilter(GlobalData.FilterHandle);
|
||||
GlobalData.FilterHandle = NULL;
|
||||
}
|
||||
|
||||
UnloadRules();
|
||||
UninitializeRulesEngine();
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ Signature = "$WINDOWS NT$"
|
||||
Class = ActivityMonitor
|
||||
ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2}
|
||||
Provider = %ManufacturerName%
|
||||
DriverVer = 12/29/2025,1.0.0.0
|
||||
DriverVer = 04/26/2026,3.5.1.0
|
||||
CatalogFile = PYAS_Driver.cat
|
||||
PnpLockdown = 1
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ FLT_PREOP_CALLBACK_STATUS ProtectBoot_PreDeviceControl(PFLT_CALLBACK_DATA Data,
|
||||
UNREFERENCED_PARAMETER(CompletionContext);
|
||||
|
||||
if (Data->RequestorMode == KernelMode) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
if (KeGetCurrentIrql() > APC_LEVEL) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
ULONG IoControlCode = Data->Iopb->Parameters.DeviceIoControl.Common.IoControlCode;
|
||||
@@ -23,7 +22,7 @@ FLT_PREOP_CALLBACK_STATUS ProtectBoot_PreDeviceControl(PFLT_CALLBACK_DATA Data,
|
||||
IoControlCode == IOCTL_DISK_FORMAT_TRACKS_EX) {
|
||||
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid) && !IsInstallerProcess(Pid)) {
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
UNICODE_STRING MsgStr = RTL_CONSTANT_STRING(L"Disk_Wiper_Attempt");
|
||||
SendMessageToUser(4001, (ULONG)(ULONG_PTR)Pid, MsgStr.Buffer, MsgStr.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
|
||||
+115
-59
@@ -34,15 +34,23 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_R
|
||||
|
||||
FileName = nameInfo->Name;
|
||||
if (!FileName.Buffer || FileName.Length == 0) {
|
||||
goto cleanup;
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
}
|
||||
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
BOOLEAN Trusted = IsProcessTrusted(Pid);
|
||||
|
||||
ULONG CreateDisposition = (Data->Iopb->Parameters.Create.Options >> 24) & 0xFF;
|
||||
BOOLEAN IsCreateAction = (CreateDisposition == FILE_CREATE ||
|
||||
CreateDisposition == FILE_SUPERSEDE ||
|
||||
CreateDisposition == FILE_OVERWRITE ||
|
||||
CreateDisposition == FILE_OVERWRITE_IF ||
|
||||
CreateDisposition == FILE_OPEN_IF);
|
||||
|
||||
if (IsHoneyToken(&FileName)) {
|
||||
if (!Trusted) {
|
||||
if (Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess & (FILE_WRITE_DATA | DELETE)) {
|
||||
if ((Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess & (FILE_WRITE_DATA | DELETE)) || IsCreateAction) {
|
||||
SendMessageToUser(5001, (ULONG)(ULONG_PTR)Pid, FileName.Buffer, FileName.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
@@ -57,7 +65,7 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_R
|
||||
if (Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess &
|
||||
(FILE_WRITE_DATA | FILE_APPEND_DATA | GENERIC_WRITE | WRITE_DAC | WRITE_OWNER)) {
|
||||
|
||||
if (!Trusted && !IsInstallerProcess(Pid)) {
|
||||
if (!Trusted) {
|
||||
UNICODE_STRING MsgStr = RTL_CONSTANT_STRING(L"Disk_Wiper_Attempt");
|
||||
SendMessageToUser(4001, (ULONG)(ULONG_PTR)Pid, MsgStr.Buffer, MsgStr.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
@@ -69,7 +77,8 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_R
|
||||
|
||||
if (CheckProtectedPathRule(&FileName)) {
|
||||
if (!Trusted) {
|
||||
if (Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess & (FILE_WRITE_DATA | DELETE | WRITE_DAC | GENERIC_WRITE)) {
|
||||
if ((Data->Iopb->Parameters.Create.SecurityContext->DesiredAccess &
|
||||
(FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES | DELETE | WRITE_DAC | GENERIC_WRITE)) || IsCreateAction) {
|
||||
SendMessageToUser(2001, (ULONG)(ULONG_PTR)Pid, FileName.Buffer, FileName.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
@@ -114,39 +123,83 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_PreSetInfo(PFLT_CALLBACK_DATA Data, PCFLT_
|
||||
}
|
||||
|
||||
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
|
||||
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
NTSTATUS status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo);
|
||||
|
||||
if (!NT_SUCCESS(status) || !nameInfo) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
if (!NT_SUCCESS(status) || !nameInfo) goto cleanup;
|
||||
|
||||
status = FltParseFileNameInformation(nameInfo);
|
||||
if (!NT_SUCCESS(status) || !nameInfo->Name.Buffer) {
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
}
|
||||
if (!NT_SUCCESS(status) || !nameInfo->Name.Buffer) goto cleanup;
|
||||
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
if (CheckFileExtensionRule(&nameInfo->Name) || CheckProtectedPathRule(&nameInfo->Name)) {
|
||||
if (CheckProtectedPathRule(&nameInfo->Name)) {
|
||||
SendMessageToUser(2001, (ULONG)(ULONG_PTR)Pid, nameInfo->Name.Buffer, nameInfo->Name.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (CheckFileExtensionRule(&nameInfo->Name)) {
|
||||
if (CheckRansomActivity(Pid, &nameInfo->Name, NULL, 0, FALSE)) {
|
||||
SendMessageToUser(5001, (ULONG)(ULONG_PTR)Pid, nameInfo->Name.Buffer, nameInfo->Name.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
return FLT_PREOP_COMPLETE;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (nameInfo) {
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
}
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
return callbackStatus;
|
||||
}
|
||||
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_PreWrite(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext) {
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_SetSecurity(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext) {
|
||||
UNREFERENCED_PARAMETER(FltObjects);
|
||||
UNREFERENCED_PARAMETER(CompletionContext);
|
||||
|
||||
if (Data->RequestorMode == KernelMode) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
if (KeGetCurrentIrql() > APC_LEVEL) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
|
||||
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
NTSTATUS status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo);
|
||||
|
||||
if (!NT_SUCCESS(status) || !nameInfo) goto cleanup;
|
||||
|
||||
status = FltParseFileNameInformation(nameInfo);
|
||||
if (!NT_SUCCESS(status) || !nameInfo->Name.Buffer) goto cleanup;
|
||||
|
||||
SECURITY_INFORMATION SecurityInfo = Data->Iopb->Parameters.SetSecurity.SecurityInformation;
|
||||
|
||||
if (SecurityInfo & (OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION)) {
|
||||
if (CheckProtectedPathRule(&nameInfo->Name)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
SendMessageToUser(2001, (ULONG)(ULONG_PTR)Pid, nameInfo->Name.Buffer, nameInfo->Name.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (nameInfo) {
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
}
|
||||
return callbackStatus;
|
||||
}
|
||||
|
||||
FLT_PREOP_CALLBACK_STATUS ProtectFile_PreWrite(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* CompletionContext) {
|
||||
UNREFERENCED_PARAMETER(CompletionContext);
|
||||
UNREFERENCED_PARAMETER(FltObjects);
|
||||
|
||||
if (Data->RequestorMode == KernelMode) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
if (KeGetCurrentIrql() > APC_LEVEL) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
if (Data->Iopb->IrpFlags & (IRP_PAGING_IO | IRP_SYNCHRONOUS_PAGING_IO | IRP_NOCACHE)) {
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
@@ -156,46 +209,43 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_PreWrite(PFLT_CALLBACK_DATA Data, PCFLT_RE
|
||||
if (WriteLength == 0) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
|
||||
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
NTSTATUS status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo);
|
||||
|
||||
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
if (!NT_SUCCESS(status) || !nameInfo) goto cleanup;
|
||||
|
||||
if (NT_SUCCESS(status) && nameInfo) {
|
||||
status = FltParseFileNameInformation(nameInfo);
|
||||
if (NT_SUCCESS(status) && nameInfo->Name.Buffer) {
|
||||
status = FltParseFileNameInformation(nameInfo);
|
||||
if (!NT_SUCCESS(status) || !nameInfo->Name.Buffer) goto cleanup;
|
||||
|
||||
if (CheckFileExtensionRule(&nameInfo->Name)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (CheckFileExtensionRule(&nameInfo->Name)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
PVOID SystemBuffer = NULL;
|
||||
PMDL Mdl = Data->Iopb->Parameters.Write.MdlAddress;
|
||||
|
||||
PVOID SystemBuffer = NULL;
|
||||
PMDL Mdl = Data->Iopb->Parameters.Write.MdlAddress;
|
||||
|
||||
if (!Mdl) {
|
||||
if (Data->Iopb->Parameters.Write.WriteBuffer && KeGetCurrentIrql() <= APC_LEVEL) {
|
||||
if (NT_SUCCESS(FltLockUserBuffer(Data))) {
|
||||
Mdl = Data->Iopb->Parameters.Write.MdlAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Mdl) {
|
||||
SystemBuffer = MmGetSystemAddressForMdlSafe(Mdl, NormalPagePriority | MdlMappingNoExecute);
|
||||
}
|
||||
|
||||
if (SystemBuffer) {
|
||||
if (CheckRansomActivity(Pid, &nameInfo->Name, SystemBuffer, WriteLength, TRUE)) {
|
||||
if (!IsInstallerProcess(Pid)) {
|
||||
SendMessageToUser(5001, (ULONG)(ULONG_PTR)Pid, nameInfo->Name.Buffer, nameInfo->Name.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
}
|
||||
}
|
||||
if (!Mdl) {
|
||||
if (Data->Iopb->Parameters.Write.WriteBuffer && KeGetCurrentIrql() <= APC_LEVEL) {
|
||||
if (NT_SUCCESS(FltLockUserBuffer(Data))) {
|
||||
Mdl = Data->Iopb->Parameters.Write.MdlAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Mdl) {
|
||||
SystemBuffer = MmGetSystemAddressForMdlSafe(Mdl, NormalPagePriority | MdlMappingNoExecute);
|
||||
}
|
||||
|
||||
if (CheckRansomActivity(Pid, &nameInfo->Name, SystemBuffer, WriteLength, TRUE)) {
|
||||
SendMessageToUser(5001, (ULONG)(ULONG_PTR)Pid, nameInfo->Name.Buffer, nameInfo->Name.Length);
|
||||
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (nameInfo) {
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
}
|
||||
return callbackStatus;
|
||||
@@ -212,25 +262,31 @@ FLT_PREOP_CALLBACK_STATUS ProtectFile_FileSystemControl(PFLT_CALLBACK_DATA Data,
|
||||
PFS_BPIO_INPUT InputBuffer = (PFS_BPIO_INPUT)Data->Iopb->Parameters.FileSystemControl.Neither.InputBuffer;
|
||||
if (!InputBuffer) return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
__try {
|
||||
if (InputBuffer->Operation == FS_BPIO_OP_ENABLE) {
|
||||
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
|
||||
if (NT_SUCCESS(FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo)) && nameInfo) {
|
||||
if (NT_SUCCESS(FltParseFileNameInformation(nameInfo))) {
|
||||
if (CheckFileExtensionRule(&nameInfo->Name) || CheckProtectedPathRule(&nameInfo->Name)) {
|
||||
FS_BPIO_OPERATIONS Operation;
|
||||
|
||||
Data->IoStatus.Status = STATUS_NOT_SUPPORTED;
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
return FLT_PREOP_COMPLETE;
|
||||
}
|
||||
}
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
}
|
||||
}
|
||||
__try {
|
||||
Operation = InputBuffer->Operation;
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
}
|
||||
|
||||
return FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
|
||||
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
|
||||
|
||||
if (Operation == FS_BPIO_OP_ENABLE) {
|
||||
if (NT_SUCCESS(FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo)) && nameInfo) {
|
||||
if (NT_SUCCESS(FltParseFileNameInformation(nameInfo))) {
|
||||
if (CheckFileExtensionRule(&nameInfo->Name) || CheckProtectedPathRule(&nameInfo->Name)) {
|
||||
Data->IoStatus.Status = STATUS_NOT_SUPPORTED;
|
||||
callbackStatus = FLT_PREOP_COMPLETE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nameInfo) {
|
||||
FltReleaseFileNameInformation(nameInfo);
|
||||
}
|
||||
return callbackStatus;
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
#include "DriverCommon.h"
|
||||
|
||||
PVOID ObRegistrationHandle = NULL;
|
||||
OB_CALLBACK_REGISTRATION ObRegistration;
|
||||
OB_OPERATION_REGISTRATION ObCallbacks[1];
|
||||
static PVOID ObRegistrationHandle = NULL;
|
||||
static OB_CALLBACK_REGISTRATION ObRegistration;
|
||||
static OB_OPERATION_REGISTRATION ObCallbacks[2];
|
||||
|
||||
static BOOLEAN IsSystemImage(PUNICODE_STRING FullImageName) {
|
||||
if (!FullImageName || !FullImageName->Buffer) return FALSE;
|
||||
|
||||
if (WildcardMatch(L"*\\Windows\\System32\\*", FullImageName->Buffer, FullImageName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\SysWOW64\\*", FullImageName->Buffer, FullImageName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\WinSxS\\*", FullImageName->Buffer, FullImageName->Length)) {
|
||||
WildcardMatch(L"*\\Windows\\WinSxS\\*", FullImageName->Buffer, FullImageName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\Microsoft.NET\\*", FullImageName->Buffer, FullImageName->Length) ||
|
||||
WildcardMatch(L"*\\Common Files\\Microsoft Shared\\*", FullImageName->Buffer, FullImageName->Length) ||
|
||||
WildcardMatch(L"*\\Program Files*\\*", FullImageName->Buffer, FullImageName->Length)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
@@ -31,8 +34,38 @@ VOID ImageLoadNotify(PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INF
|
||||
}
|
||||
}
|
||||
|
||||
static BOOLEAN IsCriticalSystemProcess(HANDLE ProcessId) {
|
||||
if (ProcessId == (HANDLE)4) return TRUE;
|
||||
|
||||
if (KeGetCurrentIrql() != PASSIVE_LEVEL) return FALSE;
|
||||
|
||||
NTSTATUS status;
|
||||
BOOLEAN isCritical = FALSE;
|
||||
PEPROCESS Process = NULL;
|
||||
PUNICODE_STRING imageFileName = NULL;
|
||||
|
||||
status = PsLookupProcessByProcessId(ProcessId, &Process);
|
||||
if (!NT_SUCCESS(status)) return FALSE;
|
||||
|
||||
status = SeLocateProcessImageName(Process, &imageFileName);
|
||||
|
||||
if (NT_SUCCESS(status) && imageFileName && imageFileName->Buffer) {
|
||||
if (WildcardMatch(L"*\\Windows\\System32\\csrss.exe", imageFileName->Buffer, imageFileName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\System32\\lsass.exe", imageFileName->Buffer, imageFileName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\System32\\services.exe", imageFileName->Buffer, imageFileName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\System32\\wininit.exe", imageFileName->Buffer, imageFileName->Length) ||
|
||||
WildcardMatch(L"*\\Windows\\System32\\smss.exe", imageFileName->Buffer, imageFileName->Length)) {
|
||||
isCritical = TRUE;
|
||||
}
|
||||
ExFreePool(imageFileName);
|
||||
}
|
||||
|
||||
ObDereferenceObject(Process);
|
||||
return isCritical;
|
||||
}
|
||||
|
||||
static BOOLEAN IsBlacklistedAdminTool(HANDLE ProcessId) {
|
||||
if (KeGetCurrentIrql() > APC_LEVEL) return FALSE;
|
||||
if (KeGetCurrentIrql() != PASSIVE_LEVEL) return FALSE;
|
||||
|
||||
NTSTATUS status;
|
||||
BOOLEAN isBlacklisted = FALSE;
|
||||
@@ -72,22 +105,21 @@ static OB_PREOP_CALLBACK_STATUS PreOpenProcess(PVOID RegistrationContext, POB_PR
|
||||
HANDLE SourcePid = PsGetCurrentProcessId();
|
||||
|
||||
if (SourcePid == TargetPid) return OB_PREOP_SUCCESS;
|
||||
|
||||
if ((ULONG)(ULONG_PTR)SourcePid == GlobalData.PyasPid) return OB_PREOP_SUCCESS;
|
||||
if (SourcePid == (HANDLE)4) return OB_PREOP_SUCCESS;
|
||||
|
||||
BOOLEAN bProtected = IsTargetProtected(TargetPid);
|
||||
if (IsTargetProtected(TargetPid)) {
|
||||
if (IsCriticalSystemProcess(SourcePid)) return OB_PREOP_SUCCESS;
|
||||
|
||||
if (bProtected) {
|
||||
BOOLEAN bIsInstaller = IsInstallerProcess(SourcePid);
|
||||
BOOLEAN bIsTrusted = IsProcessTrusted(SourcePid);
|
||||
|
||||
if (bIsInstaller) {
|
||||
if (bIsTrusted) {
|
||||
if (IsBlacklistedAdminTool(SourcePid)) {
|
||||
bIsInstaller = FALSE;
|
||||
bIsTrusted = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (bIsInstaller) return OB_PREOP_SUCCESS;
|
||||
if (bIsTrusted) return OB_PREOP_SUCCESS;
|
||||
|
||||
ACCESS_MASK DenyMask = PROCESS_TERMINATE |
|
||||
PROCESS_VM_OPERATION |
|
||||
@@ -99,12 +131,57 @@ static OB_PREOP_CALLBACK_STATUS PreOpenProcess(PVOID RegistrationContext, POB_PR
|
||||
PROCESS_SET_INFORMATION |
|
||||
PROCESS_SET_QUOTA;
|
||||
|
||||
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~DenyMask;
|
||||
OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess &= ~DenyMask;
|
||||
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= (ACCESS_MASK)(~DenyMask);
|
||||
|
||||
if (OperationInformation->Operation == OB_OPERATION_HANDLE_DUPLICATE) {
|
||||
OperationInformation->Parameters->DuplicateHandleInformation.DesiredAccess &= ~DenyMask;
|
||||
OperationInformation->Parameters->DuplicateHandleInformation.OriginalDesiredAccess &= ~DenyMask;
|
||||
OperationInformation->Parameters->DuplicateHandleInformation.DesiredAccess &= (ACCESS_MASK)(~DenyMask);
|
||||
}
|
||||
}
|
||||
|
||||
return OB_PREOP_SUCCESS;
|
||||
}
|
||||
|
||||
static OB_PREOP_CALLBACK_STATUS PreOpenThread(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation) {
|
||||
UNREFERENCED_PARAMETER(RegistrationContext);
|
||||
|
||||
if (OperationInformation->KernelHandle) return OB_PREOP_SUCCESS;
|
||||
|
||||
PETHREAD TargetThread = (PETHREAD)OperationInformation->Object;
|
||||
if (!TargetThread) return OB_PREOP_SUCCESS;
|
||||
|
||||
PEPROCESS TargetProcess = IoThreadToProcess(TargetThread);
|
||||
if (!TargetProcess) return OB_PREOP_SUCCESS;
|
||||
|
||||
HANDLE TargetPid = PsGetProcessId(TargetProcess);
|
||||
HANDLE SourcePid = PsGetCurrentProcessId();
|
||||
|
||||
if (SourcePid == TargetPid) return OB_PREOP_SUCCESS;
|
||||
if ((ULONG)(ULONG_PTR)SourcePid == GlobalData.PyasPid) return OB_PREOP_SUCCESS;
|
||||
if (SourcePid == (HANDLE)4) return OB_PREOP_SUCCESS;
|
||||
|
||||
if (IsTargetProtected(TargetPid)) {
|
||||
if (IsCriticalSystemProcess(SourcePid)) return OB_PREOP_SUCCESS;
|
||||
|
||||
BOOLEAN bIsTrusted = IsProcessTrusted(SourcePid);
|
||||
|
||||
if (bIsTrusted) {
|
||||
if (IsBlacklistedAdminTool(SourcePid)) {
|
||||
bIsTrusted = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (bIsTrusted) return OB_PREOP_SUCCESS;
|
||||
|
||||
ACCESS_MASK DenyMask = THREAD_TERMINATE |
|
||||
THREAD_SUSPEND_RESUME |
|
||||
THREAD_SET_CONTEXT |
|
||||
THREAD_SET_INFORMATION |
|
||||
THREAD_SET_THREAD_TOKEN;
|
||||
|
||||
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= (ACCESS_MASK)(~DenyMask);
|
||||
|
||||
if (OperationInformation->Operation == OB_OPERATION_HANDLE_DUPLICATE) {
|
||||
OperationInformation->Parameters->DuplicateHandleInformation.DesiredAccess &= (ACCESS_MASK)(~DenyMask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,21 +189,33 @@ static OB_PREOP_CALLBACK_STATUS PreOpenProcess(PVOID RegistrationContext, POB_PR
|
||||
}
|
||||
|
||||
NTSTATUS InitializeProcessProtection() {
|
||||
static UNICODE_STRING Altitude = { 0 };
|
||||
RtlInitUnicodeString(&Altitude, L"320000");
|
||||
static UNICODE_STRING Altitude = RTL_CONSTANT_STRING(L"320000.PYAS.Ob");
|
||||
|
||||
ObCallbacks[0].ObjectType = PsProcessType;
|
||||
ObCallbacks[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
|
||||
ObCallbacks[0].PreOperation = PreOpenProcess;
|
||||
ObCallbacks[0].PostOperation = NULL;
|
||||
|
||||
ObCallbacks[1].ObjectType = PsThreadType;
|
||||
ObCallbacks[1].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
|
||||
ObCallbacks[1].PreOperation = PreOpenThread;
|
||||
ObCallbacks[1].PostOperation = NULL;
|
||||
|
||||
ObRegistration.Version = OB_FLT_REGISTRATION_VERSION;
|
||||
ObRegistration.OperationRegistrationCount = 1;
|
||||
ObRegistration.OperationRegistrationCount = 2;
|
||||
ObRegistration.Altitude = Altitude;
|
||||
ObRegistration.RegistrationContext = NULL;
|
||||
ObRegistration.OperationRegistration = ObCallbacks;
|
||||
|
||||
return ObRegisterCallbacks(&ObRegistration, &ObRegistrationHandle);
|
||||
NTSTATUS status = ObRegisterCallbacks(&ObRegistration, &ObRegistrationHandle);
|
||||
|
||||
if (!NT_SUCCESS(status) && (status == STATUS_FLT_INSTANCE_ALTITUDE_COLLISION || status == STATUS_OBJECT_NAME_COLLISION)) {
|
||||
static UNICODE_STRING FallbackAltitude = RTL_CONSTANT_STRING(L"320000.PYAS.Ob.Fallback");
|
||||
ObRegistration.Altitude = FallbackAltitude;
|
||||
status = ObRegisterCallbacks(&ObRegistration, &ObRegistrationHandle);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
VOID UninitializeProcessProtection() {
|
||||
|
||||
@@ -1,6 +1,73 @@
|
||||
#include "DriverCommon.h"
|
||||
|
||||
LARGE_INTEGER Cookie;
|
||||
static LARGE_INTEGER Cookie;
|
||||
|
||||
static BOOLEAN IsBamRegistryPath(PCUNICODE_STRING KeyName) {
|
||||
if (!KeyName || !KeyName->Buffer) return FALSE;
|
||||
if (WildcardMatch(L"*\\Services\\bam\\*", KeyName->Buffer, KeyName->Length)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static BOOLEAN IsWriteAccess(ACCESS_MASK Access) {
|
||||
if (Access & (KEY_SET_VALUE | KEY_CREATE_SUB_KEY | KEY_CREATE_LINK |
|
||||
DELETE | WRITE_DAC | WRITE_OWNER | GENERIC_WRITE)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static PWCHAR GetFullPath(PVOID RootObject, PUNICODE_STRING CompleteName, PULONG OutLength) {
|
||||
PWCHAR Buffer = NULL;
|
||||
ULONG TotalSize = 0;
|
||||
PCUNICODE_STRING RootName = NULL;
|
||||
BOOLEAN NeedFreeRootName = FALSE;
|
||||
|
||||
if (RootObject) {
|
||||
NTSTATUS status = CmCallbackGetKeyObjectIDEx(&Cookie, RootObject, NULL, &RootName, 0);
|
||||
if (NT_SUCCESS(status) && RootName) {
|
||||
NeedFreeRootName = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
ULONG RootLen = (RootName && RootName->Buffer) ? RootName->Length : 0;
|
||||
ULONG RelLen = (CompleteName && CompleteName->Buffer) ? CompleteName->Length : 0;
|
||||
|
||||
TotalSize = RootLen + sizeof(WCHAR) + RelLen + sizeof(WCHAR);
|
||||
|
||||
Buffer = (PWCHAR)PyasAllocate(TotalSize);
|
||||
if (!Buffer) {
|
||||
if (NeedFreeRootName) CmCallbackReleaseKeyObjectIDEx(RootName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RtlZeroMemory(Buffer, TotalSize);
|
||||
|
||||
PWCHAR Current = Buffer;
|
||||
if (RootLen > 0) {
|
||||
RtlCopyMemory(Current, RootName->Buffer, RootLen);
|
||||
Current += (RootLen / sizeof(WCHAR));
|
||||
if (Current > Buffer && *(Current - 1) != L'\\') {
|
||||
*Current = L'\\';
|
||||
Current++;
|
||||
}
|
||||
}
|
||||
|
||||
if (RelLen > 0) {
|
||||
if (RootLen > 0 && CompleteName->Buffer[0] == L'\\') {
|
||||
RtlCopyMemory(Current, CompleteName->Buffer + 1, RelLen - sizeof(WCHAR));
|
||||
}
|
||||
else {
|
||||
RtlCopyMemory(Current, CompleteName->Buffer, RelLen);
|
||||
}
|
||||
}
|
||||
|
||||
if (NeedFreeRootName) CmCallbackReleaseKeyObjectIDEx(RootName);
|
||||
|
||||
if (OutLength) *OutLength = (ULONG)wcslen(Buffer) * sizeof(WCHAR);
|
||||
return Buffer;
|
||||
}
|
||||
|
||||
static NTSTATUS RegistryCallback(PVOID CallbackContext, PVOID Argument1, PVOID Argument2) {
|
||||
UNREFERENCED_PARAMETER(CallbackContext);
|
||||
@@ -8,39 +75,175 @@ static NTSTATUS RegistryCallback(PVOID CallbackContext, PVOID Argument1, PVOID A
|
||||
if (KeGetCurrentIrql() > APC_LEVEL) return STATUS_SUCCESS;
|
||||
|
||||
REG_NOTIFY_CLASS Class = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
|
||||
if (Class == RegNtPreSetValueKey || Class == RegNtPreDeleteKey || Class == RegNtPreCreateKey) {
|
||||
REG_SET_VALUE_KEY_INFORMATION* Info = (REG_SET_VALUE_KEY_INFORMATION*)Argument2;
|
||||
PCUNICODE_STRING KeyName = NULL;
|
||||
if (Class == RegNtPreCreateKey || Class == RegNtPreOpenKey || Class == RegNtPreOpenKeyEx) {
|
||||
PVOID RootObject = NULL;
|
||||
PUNICODE_STRING CompleteName = NULL;
|
||||
ACCESS_MASK DesiredAccess = 0;
|
||||
|
||||
if (!NT_SUCCESS(CmCallbackGetKeyObjectIDEx(&Cookie, Info->Object, NULL, &KeyName, 0))) {
|
||||
if (Class == RegNtPreCreateKey) {
|
||||
REG_CREATE_KEY_INFORMATION* Info = (REG_CREATE_KEY_INFORMATION*)Argument2;
|
||||
RootObject = Info->RootObject;
|
||||
CompleteName = Info->CompleteName;
|
||||
DesiredAccess = Info->DesiredAccess;
|
||||
}
|
||||
else {
|
||||
REG_OPEN_KEY_INFORMATION* Info = (REG_OPEN_KEY_INFORMATION*)Argument2;
|
||||
RootObject = Info->RootObject;
|
||||
CompleteName = Info->CompleteName;
|
||||
DesiredAccess = Info->DesiredAccess;
|
||||
}
|
||||
|
||||
if (!IsWriteAccess(DesiredAccess)) {
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
if (KeyName && KeyName->Buffer) {
|
||||
if (CheckRegistryRule(KeyName)) {
|
||||
ULONG PathLen = 0;
|
||||
PWCHAR FullPathBuffer = GetFullPath(RootObject, CompleteName, &PathLen);
|
||||
|
||||
if (FullPathBuffer) {
|
||||
UNICODE_STRING FullPath;
|
||||
RtlInitUnicodeString(&FullPath, FullPathBuffer);
|
||||
|
||||
if (CheckRegistryRule(&FullPath)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
if (!IsInstallerProcess(Pid)) {
|
||||
SendMessageToUser(3001, (ULONG)(ULONG_PTR)Pid, KeyName->Buffer, KeyName->Length);
|
||||
CmCallbackReleaseKeyObjectIDEx(KeyName);
|
||||
return STATUS_ACCESS_DENIED;
|
||||
}
|
||||
SendMessageToUser(3001, (ULONG)(ULONG_PTR)Pid, FullPath.Buffer, FullPath.Length);
|
||||
status = STATUS_ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
PyasFree(FullPathBuffer);
|
||||
}
|
||||
|
||||
if (KeyName) {
|
||||
CmCallbackReleaseKeyObjectIDEx(KeyName);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
if (Class == RegNtPreDeleteKey) {
|
||||
REG_DELETE_KEY_INFORMATION* Info = (REG_DELETE_KEY_INFORMATION*)Argument2;
|
||||
PCUNICODE_STRING KeyName = NULL;
|
||||
|
||||
if (NT_SUCCESS(CmCallbackGetKeyObjectIDEx(&Cookie, Info->Object, NULL, &KeyName, 0))) {
|
||||
if (KeyName) {
|
||||
if (CheckRegistryRule(KeyName)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
SendMessageToUser(3001, (ULONG)(ULONG_PTR)Pid, KeyName->Buffer, KeyName->Length);
|
||||
status = STATUS_ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
CmCallbackReleaseKeyObjectIDEx(KeyName);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
if (Class == RegNtPreSetValueKey) {
|
||||
REG_SET_VALUE_KEY_INFORMATION* Info = (REG_SET_VALUE_KEY_INFORMATION*)Argument2;
|
||||
PCUNICODE_STRING KeyName = NULL;
|
||||
|
||||
if (NT_SUCCESS(CmCallbackGetKeyObjectIDEx(&Cookie, Info->Object, NULL, &KeyName, 0))) {
|
||||
if (KeyName) {
|
||||
if (KeyName->Buffer) {
|
||||
if (!IsBamRegistryPath(KeyName)) {
|
||||
ULONG FullSize = KeyName->Length + sizeof(WCHAR) +
|
||||
(Info->ValueName ? Info->ValueName->Length : 0) +
|
||||
sizeof(WCHAR);
|
||||
|
||||
if (FullSize <= 0xFFFF) {
|
||||
PWCHAR Buffer = (PWCHAR)PyasAllocate(FullSize);
|
||||
if (Buffer) {
|
||||
UNICODE_STRING FullPath;
|
||||
FullPath.Buffer = Buffer;
|
||||
FullPath.Length = 0;
|
||||
FullPath.MaximumLength = (USHORT)FullSize;
|
||||
|
||||
RtlCopyUnicodeString(&FullPath, KeyName);
|
||||
|
||||
if (Info->ValueName && Info->ValueName->Length > 0) {
|
||||
if (FullPath.Length > 0 && FullPath.Buffer[(FullPath.Length / sizeof(WCHAR)) - 1] != L'\\') {
|
||||
RtlAppendUnicodeToString(&FullPath, L"\\");
|
||||
}
|
||||
RtlAppendUnicodeStringToString(&FullPath, Info->ValueName);
|
||||
}
|
||||
|
||||
if (CheckRegistryRule(&FullPath)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
SendMessageToUser(3001, (ULONG)(ULONG_PTR)Pid, FullPath.Buffer, FullPath.Length);
|
||||
status = STATUS_ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
PyasFree(Buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CmCallbackReleaseKeyObjectIDEx(KeyName);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
if (Class == RegNtPreDeleteValueKey) {
|
||||
REG_DELETE_VALUE_KEY_INFORMATION* Info = (REG_DELETE_VALUE_KEY_INFORMATION*)Argument2;
|
||||
PCUNICODE_STRING KeyName = NULL;
|
||||
|
||||
if (NT_SUCCESS(CmCallbackGetKeyObjectIDEx(&Cookie, Info->Object, NULL, &KeyName, 0))) {
|
||||
if (KeyName) {
|
||||
if (KeyName->Buffer) {
|
||||
if (!IsBamRegistryPath(KeyName)) {
|
||||
ULONG FullSize = KeyName->Length + sizeof(WCHAR) +
|
||||
(Info->ValueName ? Info->ValueName->Length : 0) +
|
||||
sizeof(WCHAR);
|
||||
|
||||
if (FullSize <= 0xFFFF) {
|
||||
PWCHAR Buffer = (PWCHAR)PyasAllocate(FullSize);
|
||||
if (Buffer) {
|
||||
UNICODE_STRING FullPath;
|
||||
FullPath.Buffer = Buffer;
|
||||
FullPath.Length = 0;
|
||||
FullPath.MaximumLength = (USHORT)FullSize;
|
||||
|
||||
RtlCopyUnicodeString(&FullPath, KeyName);
|
||||
|
||||
if (Info->ValueName && Info->ValueName->Length > 0) {
|
||||
if (FullPath.Length > 0 && FullPath.Buffer[(FullPath.Length / sizeof(WCHAR)) - 1] != L'\\') {
|
||||
RtlAppendUnicodeToString(&FullPath, L"\\");
|
||||
}
|
||||
RtlAppendUnicodeStringToString(&FullPath, Info->ValueName);
|
||||
}
|
||||
|
||||
if (CheckRegistryRule(&FullPath)) {
|
||||
HANDLE Pid = PsGetCurrentProcessId();
|
||||
if (!IsProcessTrusted(Pid)) {
|
||||
SendMessageToUser(3001, (ULONG)(ULONG_PTR)Pid, FullPath.Buffer, FullPath.Length);
|
||||
status = STATUS_ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
PyasFree(Buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CmCallbackReleaseKeyObjectIDEx(KeyName);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
NTSTATUS InitializeRegistryProtection(PDRIVER_OBJECT DriverObject) {
|
||||
UNICODE_STRING Altitude;
|
||||
RtlInitUnicodeString(&Altitude, L"320000");
|
||||
return CmRegisterCallbackEx(RegistryCallback, &Altitude, DriverObject, NULL, &Cookie, NULL);
|
||||
static UNICODE_STRING Altitude = RTL_CONSTANT_STRING(L"320000.PYAS.Cm");
|
||||
NTSTATUS status = CmRegisterCallbackEx(RegistryCallback, &Altitude, DriverObject, NULL, &Cookie, NULL);
|
||||
|
||||
if (!NT_SUCCESS(status)) {
|
||||
static UNICODE_STRING FallbackAltitude = RTL_CONSTANT_STRING(L"320000.PYAS.Cm.Fallback");
|
||||
status = CmRegisterCallbackEx(RegistryCallback, &FallbackAltitude, DriverObject, NULL, &Cookie, NULL);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
VOID UninitializeRegistryProtection() {
|
||||
|
||||
+753
-307
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
||||
#include <windows.h>
|
||||
#include <ntverp.h>
|
||||
|
||||
#define VER_FILETYPE VFT_DRV
|
||||
#define VER_FILESUBTYPE VFT2_DRV_SYSTEM
|
||||
#define VER_FILEDESCRIPTION_STR "PYAS Security Driver Protect"
|
||||
#define VER_INTERNALNAME_STR "PYAS"
|
||||
#define VER_LEGALCOPYRIGHT_STR "PYAS Security"
|
||||
#define VER_ORIGINALFILENAME_STR "PYAS_Driver.sys"
|
||||
#define VER_PRODUCTNAME_STR "PYAS"
|
||||
|
||||
#define VER_FILEVERSION 3,5,1,0
|
||||
#define VER_FILEVERSION_STR "3,5,1,0"
|
||||
|
||||
#define VER_PRODUCTVERSION 3,5,1,0
|
||||
#define VER_PRODUCTVERSION_STR "3,5,1,0"
|
||||
|
||||
#define VER_COMPANYNAME_STR "PYAS Security"
|
||||
|
||||
#include "common.ver"
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"Rule_Registry_BlockList": [
|
||||
"\\REGISTRY\\MACHINE\\BCD00000000",
|
||||
"\\REGISTRY\\MACHINE\\BCD00000000\\*",
|
||||
|
||||
"*\\Classes\\.exe",
|
||||
"*\\Classes\\.bat",
|
||||
"*\\Classes\\.cmd",
|
||||
"*\\Classes\\.com",
|
||||
"*\\Classes\\exefile\\*",
|
||||
"*\\Classes\\batfile\\*",
|
||||
"*\\Classes\\cmdfile\\*",
|
||||
"*\\Classes\\comfile\\*",
|
||||
|
||||
"*\\mouclass",
|
||||
"*\\kbdclass",
|
||||
"*\\DisableAntiSpyware",
|
||||
"*\\DisableWindowsUpdateAccess",
|
||||
"*\\EnableLUA",
|
||||
"*\\ConsentPromptBehaviorAdmin",
|
||||
"*\\NoControlPanel",
|
||||
"*\\NoDrives",
|
||||
"*\\NoFileMenu",
|
||||
"*\\NoFind",
|
||||
"*\\NoStartMenuPinnedList",
|
||||
"*\\NoSetFolders",
|
||||
"*\\NoSetFolderOptions",
|
||||
"*\\NoViewOnDrive",
|
||||
"*\\NoClose",
|
||||
"*\\NoDesktop",
|
||||
"*\\NoLogoff",
|
||||
"*\\NoFolderOptions",
|
||||
"*\\RestrictRun",
|
||||
"*\\NoViewContextMenu",
|
||||
"*\\HideClock",
|
||||
"*\\NoStartMenuMyGames",
|
||||
"*\\NoStartMenuMyMusic",
|
||||
"*\\DisableCMD",
|
||||
"*\\NoAddingComponents",
|
||||
"*\\NoWinKeys",
|
||||
"*\\NoStartMenuLogOff",
|
||||
"*\\NoSimpleNetIDList",
|
||||
"*\\NoLowDiskSpaceChecks",
|
||||
"*\\DisableLockWorkstation",
|
||||
"*\\Restrict_Run",
|
||||
"*\\DisableTaskMgr",
|
||||
"*\\DisableRegistryTools",
|
||||
"*\\DisableChangePassword",
|
||||
"*\\NoComponents",
|
||||
"*\\NoStartMenuMorePrograms",
|
||||
"*\\NoActiveDesktop",
|
||||
"*\\NoSetActiveDesktop",
|
||||
"*\\NoRecentDocsMenu",
|
||||
"*\\NoWindowsUpdate",
|
||||
"*\\NoChangeStartMenu",
|
||||
"*\\NoFavoritesMenu",
|
||||
"*\\NoRecentDocsHistory",
|
||||
"*\\NoSetTaskbar",
|
||||
"*\\NoSMHelp",
|
||||
"*\\NoTrayContextMenu",
|
||||
"*\\NoManageMyComputerVerb",
|
||||
"*\\NoRealMode",
|
||||
"*\\NoRun",
|
||||
"*\\ClearRecentDocsOnExit",
|
||||
"*\\NoActiveDesktopChanges",
|
||||
"*\\NoStartMenuNetworkPlaces",
|
||||
"*\\NoLockScreen",
|
||||
"*\\HideFastUserSwitching",
|
||||
"*\\ConsentPromptBehaviorUser",
|
||||
"*\\EnableSecureUIAPaths",
|
||||
"*\\NoSRM",
|
||||
"*\\NoInteractiveServices",
|
||||
"*\\ShutdownWithoutLogon",
|
||||
"*\\AutoAdminLogon",
|
||||
"*\\BootExecute",
|
||||
"*\\SubSystems",
|
||||
"*\\FirmwareBootDevice",
|
||||
"*\\BootDriverFlags",
|
||||
"*\\SystemStartOptions",
|
||||
"*\\DisableAutomaticRestartOnFailure",
|
||||
"*\\SystemBiosVersion",
|
||||
"*\\NoTheme",
|
||||
"*\\SystemSetupInProgress",
|
||||
"*\\SetupType",
|
||||
"*\\AutoReboot",
|
||||
"*\\Userinit",
|
||||
"*\\UIHost",
|
||||
"*\\Debugger",
|
||||
"*\\DefaultUserName",
|
||||
"*\\DefaultPassword",
|
||||
"*\\AltDefaultUserName",
|
||||
"*\\AltDefaultPassword",
|
||||
"*\\DisableCAD",
|
||||
"*\\LegalNoticeCaption",
|
||||
"*\\LegalNoticeText",
|
||||
],
|
||||
"Rule_Registry_TrustedList": [
|
||||
],
|
||||
"Rule_Process_TrustedPaths": [
|
||||
"\\Device\\HarddiskVolume*\\Windows\\*",
|
||||
"\\Device\\HarddiskVolume*\\Program Files\\*",
|
||||
"\\Device\\HarddiskVolume*\\Program Files (x86)\\*",
|
||||
|
||||
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender\\*",
|
||||
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\*",
|
||||
"*\\Sources\\SetupHost.exe",
|
||||
"*\\Work\\*\\DismHost.exe",
|
||||
|
||||
"*\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"*\\Microsoft\\Edge\\Application\\msedge.exe",
|
||||
"*\\Mozilla Firefox\\firefox.exe",
|
||||
"*\\Thorium\\Application\\thorium.exe",
|
||||
|
||||
"*\\Steam\\steam.exe",
|
||||
"*\\steam\\*\\steam.exe",
|
||||
"*\\Battle.net\\*\\Battle.net.exe",
|
||||
"*\\Epic Games\\Launcher\\Portal\\Binaries\\*\\EpicGamesLauncher.exe",
|
||||
|
||||
"*\\resources\\bin\\SDK\\DownloadSDKServer.exe",
|
||||
"*\\ThunderEv.exe",
|
||||
"*\\ThunderStart.exe",
|
||||
"*\\KuGou.exe",
|
||||
"*\\XLGame.exe",
|
||||
"*\\XLLiveUD.exe",
|
||||
|
||||
"*\\hrkill-*.exe",
|
||||
"*\\bin\\HipsDaemon.exe",
|
||||
"*\\360Safe.exe",
|
||||
"*\\bilibili\\bilibili.exe",
|
||||
|
||||
"*\\Kingsoft\\WPS Office\\ksolaunch.exe",
|
||||
"*\\Kingsoft\\WPS Office\\*\\wps.exe",
|
||||
|
||||
"*\\PC4399_WPFLauncher.exe",
|
||||
"*\\bin\\APlayer.exe",
|
||||
"*\\Neverness To Everness\\NTELauncher.exe",
|
||||
"*\\Application\\AndrowsLauncher.exe",
|
||||
"*\\bin\\*\\codex.exe",
|
||||
"*\\bin\\codex.exe",
|
||||
],
|
||||
"Rule_Process_ExploitableBlacklist": [
|
||||
"*\\cmd.exe",
|
||||
"*\\powershell.exe",
|
||||
"*\\pwsh.exe",
|
||||
"*\\wscript.exe",
|
||||
"*\\cscript.exe",
|
||||
"*\\mshta.exe",
|
||||
"*\\regsvr32.exe",
|
||||
"*\\certutil.exe",
|
||||
"*\\bitsadmin.exe",
|
||||
"*\\wmic.exe",
|
||||
"*\\hh.exe",
|
||||
"*\\cmstp.exe",
|
||||
"*\\installutil.exe",
|
||||
"*\\regasm.exe",
|
||||
"*\\regsvcs.exe",
|
||||
"*\\msbuild.exe",
|
||||
"*\\csc.exe",
|
||||
"*\\vbc.exe",
|
||||
"*\\jsc.exe",
|
||||
"*\\bash.exe",
|
||||
"*\\scrcons.exe",
|
||||
"*\\schtasks.exe",
|
||||
"*\\at.exe",
|
||||
"*\\reg.exe",
|
||||
"*\\sc.exe",
|
||||
"*\\net.exe",
|
||||
"*\\net1.exe",
|
||||
"*\\whoami.exe",
|
||||
"*\\ipconfig.exe",
|
||||
"*\\taskkill.exe",
|
||||
"*\\ProcessHacker.exe",
|
||||
"*\\procmon.exe",
|
||||
"*\\procexp.exe",
|
||||
],
|
||||
"Rule_File_ProtectedPaths": [
|
||||
"*\\PYAS.exe",
|
||||
"*\\Plugins\\Filter\\PYAS_Driver.sys",
|
||||
"*\\Plugins\\Rules\\Rules_Driver_P1.json",
|
||||
"*\\ProgramData\\PYAS\\*.json",
|
||||
"*\\Engine\\Pattern\\*.onnx",
|
||||
"*\\Engine\\Properties\\*.onnx",
|
||||
|
||||
"\\Device\\HarddiskVolume*\\bootmgr",
|
||||
"\\Device\\HarddiskVolume*\\boot.ini",
|
||||
"\\Device\\HarddiskVolume*\\BOOTNXT",
|
||||
"\\Device\\HarddiskVolume*\\EFI\\*",
|
||||
"\\Device\\HarddiskVolume*\\Boot\\*",
|
||||
"\\Device\\HarddiskVolume*\\Recovery\\*",
|
||||
"\\Device\\HarddiskVolume*\\System Volume Information\\*",
|
||||
"\\Device\\HarddiskVolume*\\Windows\\System32\\config\\*",
|
||||
|
||||
"*\\CON",
|
||||
"*\\CON\\*",
|
||||
"*\\PRN",
|
||||
"*\\PRN\\*",
|
||||
"*\\AUX",
|
||||
"*\\AUX\\*",
|
||||
"*\\NUL",
|
||||
"*\\NUL\\*",
|
||||
"*\\COM?",
|
||||
"*\\COM?\\*",
|
||||
"*\\LPT?",
|
||||
"*\\LPT?\\*",
|
||||
"*\\OSDATA",
|
||||
"*\\OSDATA\\*",
|
||||
"*\\*<*",
|
||||
"*\\*>*",
|
||||
],
|
||||
"Rule_File_ExceptionPaths": [
|
||||
"*\\Users\\*\\steam\\*",
|
||||
"*\\AppData\\*",
|
||||
"*\\Temp\\*",
|
||||
"*\\Local\\Temp\\*",
|
||||
"*\\ventoy\\Boot\\*",
|
||||
"*\\AdminRegionConfig.*",
|
||||
"*\\nt_qq\\nt_*\\*",
|
||||
"*\\cache\\*",
|
||||
],
|
||||
"Rule_File_RansomwareExtensions": [
|
||||
".zip", ".7z", ".rar", ".tar", ".gz",
|
||||
".ppt", ".pptx", ".wps", ".txt", ".rtf",
|
||||
".pdf", ".xls", ".xlsx", ".doc", ".docx",
|
||||
".jpg", ".jpeg", ".png", ".webp", ".gif",
|
||||
".mp3", ".wav", ".aac", ".ogg", ".flac",
|
||||
".mp4", ".avi", ".mov", ".wmv", ".mkv",
|
||||
]
|
||||
}
|
||||
@@ -1,22 +1,32 @@
|
||||
# PYAS
|
||||
# PYAS Security
|
||||
|
||||
Antivirus software written in Python and C++ that blocks threats through deep learning and behavioral monitoring!
|
||||
Antivirus software written in Python and C++ that blocks threats through Machine Learning and behavioral monitoring!
|
||||
|
||||
<img width="2245" height="1477" alt="PYAS_UI" src="https://github.com/user-attachments/assets/87d40261-7655-49ad-a19c-1ffcca60584f" />
|
||||
<img width="2440" height="1600" alt="PYAS_All" src="https://github.com/user-attachments/assets/1991aad3-64cc-4266-ac67-dab70c891ce7" />
|
||||
|
||||
## Requirements
|
||||
|
||||
Python 3.10 is recommended. Other Python versions may require different pip commands.
|
||||
|
||||
```
|
||||
pip install numpy==1.26.4
|
||||
pip install onnxruntime==1.18.1
|
||||
pip install pefile==2023.2.7
|
||||
pip install Pillow==11.0.0
|
||||
pip install pyperclip==1.9.0
|
||||
pip install PySide6==6.9.1
|
||||
pip install requests==2.32.4
|
||||
pip install yara-python==4.5.4
|
||||
pip install pystray
|
||||
pip install pefile
|
||||
pip install requests
|
||||
pip install pywebview
|
||||
pip install Pillow
|
||||
pip install yara-python
|
||||
pip install numpy
|
||||
pip install onnxruntime
|
||||
```
|
||||
|
||||
Non-essential requirements installation, only used for model training or other functions.
|
||||
|
||||
```
|
||||
pip install pandas
|
||||
pip install scikit-learn
|
||||
pip install lightgbm
|
||||
pip install onnxmltools
|
||||
pip install orjson
|
||||
```
|
||||
|
||||
## File Information
|
||||
@@ -25,33 +35,28 @@ The following lists the storage locations of all relevant code and other related
|
||||
|
||||
```
|
||||
PYAS/
|
||||
├── Engine/
|
||||
│ ├── Models/
|
||||
│ │ ├── convert.py # Convert executable files or other files to images
|
||||
│ │ ├── train.py # TensorFlow CNN model training complete code
|
||||
│ │ └── ... # Other models folders and files
|
||||
│ │
|
||||
├── Engine/ # Complete code for Yara signatures and AI model training
|
||||
│ ├── Heuristic/
|
||||
│ │ └── ... # Yara Rules
|
||||
│ └── Properties/
|
||||
│ └── ... # LightGBM Model
|
||||
│
|
||||
├── Interface/ # Interface Interaction and Icons with WebView2
|
||||
│ ├── static/
|
||||
│ │ └── ... # style.css, main.js, icon.ico
|
||||
│ └── templates/
|
||||
│ └── ... # index.html
|
||||
│
|
||||
├── Plugins/ # Main filter driver protect and rules
|
||||
│ ├── Filter/
|
||||
│ │ └── ... # DriverCommon.h, DriverEntry.cpp, ProtectRegistry.cpp, ...
|
||||
│ └── Rules/
|
||||
│ ├── rules.yar # Yara virus signature rule matching
|
||||
│ ├── rules.ips # IP network address rule matching
|
||||
│ └── ... # Other rules folders and files
|
||||
│
|
||||
├── Plugins/
|
||||
│ └── Filter/
|
||||
│ ├── DriverEntry.cpp # Main driver entry and initialization logic
|
||||
│ ├── DriverCommon.h # Global driver definitions, constants, and functions
|
||||
│ ├── ProtectBoot.cpp # Disk boot sector write protection
|
||||
│ ├── ProtectRegistry.cpp # Registry modification protection
|
||||
│ ├── ProtectRules.cpp # White, block list, and matching logic for files registry
|
||||
│ └── ... # Other driver folders and files
|
||||
│
|
||||
├── PYAS.py # Main application entry point and UI to engine interface
|
||||
├── PYAS_Config.py # Configuration loading, saving, and global parameters
|
||||
├── PYAS_Engine.py # Core scanning engine: YARA, IP, ONNX model execution
|
||||
├── PYAS_Interface.py # User interface components and event handling
|
||||
├── PYAS_Resource.py # Static image and icon resource management
|
||||
│ └── ... # Rules_Driver_P1.json
|
||||
|
|
||||
├── PYAS.py # Main antivirus application entry point
|
||||
├── PYAS_Engine.py # Core scanning engine algorithm
|
||||
├── PYAS_Version.py # Version metadata for packaging and updates
|
||||
└── ... # Other supplementary folders and files
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Architecture diagram
|
||||
@@ -61,122 +66,83 @@ PYAS Security antivirus software general architecture diagram.
|
||||
```mermaid
|
||||
graph TD
|
||||
%% Global Styles
|
||||
classDef ui fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
|
||||
classDef logic fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
|
||||
classDef engine fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
|
||||
classDef thread fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
|
||||
classDef kernel fill:#ffebee,stroke:#c62828,stroke-width:2px;
|
||||
classDef data fill:#eceff1,stroke:#455a64,stroke-width:2px,stroke-dasharray: 5 5;
|
||||
classDef userMode fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1
|
||||
classDef kernelMode fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100
|
||||
classDef storage fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
|
||||
classDef interaction fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,stroke-dasharray: 5 5,color:#1b5e20
|
||||
|
||||
User((User))
|
||||
|
||||
subgraph Presentation_Layer ["Presentation Layer (View)"]
|
||||
direction TB
|
||||
UI_Static["PYAS_Interface.py<br/>(Ui_main_window Class)"]:::ui
|
||||
UI_Controller["PYAS.py<br/>(MainWindow_Controller Class)"]:::ui
|
||||
TrayIcon["System Tray Icon"]:::ui
|
||||
subgraph UserMode ["User Mode (Python / Web)"]
|
||||
UI["Web UI (pywebview)"]:::userMode
|
||||
PYASCore["PYAS Core (PYAS.py)"]:::userMode
|
||||
|
||||
User <--> UI_Controller
|
||||
UI_Static --- UI_Controller
|
||||
UI_Controller --- TrayIcon
|
||||
end
|
||||
|
||||
subgraph Core_Logic_Layer ["Core Logic & Controller"]
|
||||
direction TB
|
||||
Init_Env["Initialization<br/>(init_environ, init_variable,<br/>init_windll)"]:::logic
|
||||
Config_Mgr["Configuration Manager<br/>(load_config, save_config)"]:::logic
|
||||
|
||||
subgraph System_Tools ["System Tools"]
|
||||
Sys_Repair["System Repair<br/>(repair_system_mbr,<br/>repair_system_image, etc.)"]:::logic
|
||||
Sys_Clean["System Cleaner<br/>(clean_button, traverse_temp)"]:::logic
|
||||
Proc_Mgr["Process Manager<br/>(list_process, kill_process)"]:::logic
|
||||
List_Mgr["List Manager<br/>(manage_named_list:<br/>Whitelist/Quarantine)"]:::logic
|
||||
subgraph Engines ["Scanning Engines (PYAS_Engine.py)"]
|
||||
PEScanner["PE/ML Scanner (pe_scanner)"]:::userMode
|
||||
RuleScanner["YARA Scanner (rule_scanner)"]:::userMode
|
||||
CloudScanner["Cloud API (cloud_scanner)"]:::userMode
|
||||
SignScanner["Signature Verify (sign_scanner)"]:::userMode
|
||||
end
|
||||
|
||||
UI_Controller --> Init_Env
|
||||
Init_Env --> Config_Mgr
|
||||
UI_Controller --> System_Tools
|
||||
end
|
||||
|
||||
subgraph Protection_Layer ["Real-time Protection Threads (Daemons)"]
|
||||
direction TB
|
||||
Thread_Proc["Process Guard Thread<br/>(protect_proc_thread)<br/>[WMI/Snapshot]"]:::thread
|
||||
Thread_File["File Guard Thread<br/>(protect_file_thread)<br/>[ReadDirectoryChangesW]"]:::thread
|
||||
Thread_Net["Network Guard Thread<br/>(protect_net_thread)<br/>[GetExtendedTcpTable]"]:::thread
|
||||
Thread_Sys["System Guard Thread<br/>(protect_system_thread)<br/>[Reg/MBR Check]"]:::thread
|
||||
Thread_Popup["Popup Blocker Thread<br/>(popup_intercept_thread)<br/>[EnumWindows]"]:::thread
|
||||
Thread_Pipe["Driver IPC Thread<br/>(pipe_server_thread)<br/>[NamedPipe Server]"]:::thread
|
||||
|
||||
Init_Env --> Thread_Proc
|
||||
Init_Env --> Thread_File
|
||||
Init_Env --> Thread_Net
|
||||
Init_Env --> Thread_Sys
|
||||
Init_Env --> Thread_Popup
|
||||
Init_Env --> Thread_Pipe
|
||||
end
|
||||
|
||||
subgraph Engine_Layer ["Detection Engine (PYAS_Engine.py)"]
|
||||
direction TB
|
||||
Scan_Worker["Scan Worker<br/>(scan_worker / scan_engine)"]:::engine
|
||||
|
||||
subgraph Scanners
|
||||
Sign_Check["Signature Scanner<br/>(sign_scanner)<br/>[WinVerifyTrust]"]:::engine
|
||||
AI_Model["AI Model Scanner<br/>(model_scanner)<br/>[ONNX/ResNet]"]:::engine
|
||||
Yara_Rules["Rule Scanner<br/>(rule_scanner)<br/>[Yara/IP Lists]"]:::engine
|
||||
CommClient["Filter Communication Client"]:::interaction
|
||||
end
|
||||
|
||||
subgraph KernelMode ["Kernel Mode (C++ Minifilter Driver)"]
|
||||
CommServer["ALPC Port (\\PYAS_Output_Pipe)"]:::interaction
|
||||
DriverCore["Driver Entry (DriverEntry.cpp)"]:::kernelMode
|
||||
RuleEngine["Rules & Trust Cache (ProtectRules.cpp)"]:::kernelMode
|
||||
|
||||
subgraph Protections ["Protection Subsystems"]
|
||||
FileProtect["File Minifilter (ProtectFile.cpp)"]:::kernelMode
|
||||
ProcessProtect["ObRegisterCallbacks (ProtectProcess.cpp)"]:::kernelMode
|
||||
RegProtect["CmRegisterCallbackEx (ProtectRegistry.cpp)"]:::kernelMode
|
||||
BootProtect["IRP_MJ_DEVICE_CONTROL (ProtectBoot.cpp)"]:::kernelMode
|
||||
end
|
||||
|
||||
Thread_Proc --> Scan_Worker
|
||||
Thread_File --> Scan_Worker
|
||||
Thread_Net --> Scan_Worker
|
||||
UI_Controller -->|Manual Scan| Scan_Worker
|
||||
|
||||
Scan_Worker --> Sign_Check
|
||||
Scan_Worker --> AI_Model
|
||||
Scan_Worker --> Yara_Rules
|
||||
end
|
||||
|
||||
subgraph Kernel_OS_Layer ["Windows Kernel & Driver"]
|
||||
direction TB
|
||||
WinAPI["Windows API (ctypes)<br/>(kernel32, user32, ntdll,<br/>advapi32, iphlpapi, psapi)"]:::kernel
|
||||
Driver_Sys["Kernel Driver<br/>(PYAS_Driver.sys)"]:::kernel
|
||||
OS_FS["File System (NTFS)"]:::kernel
|
||||
OS_Net["Network Stack"]:::kernel
|
||||
|
||||
Init_Env -- Load DLLs --> WinAPI
|
||||
Thread_File -- Monitor --> OS_FS
|
||||
Thread_Net -- Monitor --> OS_Net
|
||||
Thread_Proc -- Suspend/Kill --> WinAPI
|
||||
|
||||
Thread_Pipe <==>|"Named Pipe IPC<br/>(\\.\pipe\PYAS_Output_Pipe)"| Driver_Sys
|
||||
Driver_Sys -- Callback/Block --> WinAPI
|
||||
subgraph OS ["Windows OS & Storage"]
|
||||
FS["File System (FltMgr)"]:::storage
|
||||
Reg["Registry (CmMgr)"]:::storage
|
||||
ProcMgr["Process/Thread Manager (ObMgr)"]:::storage
|
||||
Disk["Physical Disk / MBR"]:::storage
|
||||
end
|
||||
|
||||
subgraph Data_Persistence ["Data & Resources"]
|
||||
Config_File[("Config.json")]:::data
|
||||
Model_Files[("Model Files<br/>(.onnx)")]:::data
|
||||
Rule_Files[("Rule Files<br/>(.yar)")]:::data
|
||||
Quarantine_Dir[("/Quarantine/")]:::data
|
||||
CloudAPI["PYAS Cloud Server"]:::storage
|
||||
|
||||
Config_Mgr <--> Config_File
|
||||
AI_Model <--> Model_Files
|
||||
Yara_Rules <--> Rule_Files
|
||||
List_Mgr <--> Quarantine_Dir
|
||||
end
|
||||
%% User Mode Internal
|
||||
UI <-->|JS Evaluate / JSON| PYASCore
|
||||
PYASCore -->|Extract Features & Scan| Engines
|
||||
CloudScanner -.->|HTTPS REST API| CloudAPI
|
||||
PYASCore <-->|FilterSendMessage / FilterGetMessage| CommClient
|
||||
|
||||
%% User-Kernel Boundary
|
||||
CommClient <-->|PYAS_MESSAGE / PYAS_USER_MESSAGE| CommServer
|
||||
|
||||
%% Kernel Mode Internal
|
||||
CommServer <-->|Message Dispatch| DriverCore
|
||||
DriverCore --> RuleEngine
|
||||
RuleEngine --> Protections
|
||||
|
||||
%% Kernel-OS Boundary
|
||||
FileProtect -->|IRP_MJ_CREATE, IRP_MJ_WRITE...| FS
|
||||
ProcessProtect -->|PreOpenProcess, PreOpenThread| ProcMgr
|
||||
RegProtect -->|RegNtPreCreateKey, RegNtPreSetValueKey...| Reg
|
||||
BootProtect -->|IOCTL_DISK_FORMAT_TRACKS...| Disk
|
||||
```
|
||||
|
||||
## Machine Learning
|
||||
|
||||
The machine learning-based virus scanning engine training achieved extremely high accuracy.
|
||||
|
||||
<img width="895" height="1097" alt="train" src="https://github.com/user-attachments/assets/fa7cf0e2-01a1-4234-9358-718c2b8a2c9a" />
|
||||
|
||||
## Support System
|
||||
|
||||
Please ensure your computer supports Microsoft Visual C++ 2015-2022 Redistributable and Edge Webview2.
|
||||
|
||||
| Config | Permissions | System version | Processor | Memory | Storage |
|
||||
|-----------|---------------|----------------------|-----------|--------|---------|
|
||||
| Minimum | Administrator | >= Windows 10 (20H1) | 1 GHz | 200MB | 100MB |
|
||||
| Minimum | Administrator | >= Windows 10 (20H1) | 1 GHz | 300MB | 100MB |
|
||||
| Recommend | Administrator | >= Windows 10 (21H2) | 3 GHz | 500MB | 200MB |
|
||||
|
||||
## Packaged Releases
|
||||
|
||||
Download the installer. If it is incompatible with your system, you can repackage it yourself.
|
||||
|
||||
Packaged Download: https://github.com/87owo/PYAS/releases
|
||||
|
||||
## Official Website
|
||||
|
||||
If you are interested in this project, you can visit the website to see other related content.
|
||||
@@ -185,10 +151,14 @@ Source Available : https://github.com/87owo/PYAS
|
||||
|
||||
Official Website : https://pyas-security.com/antivirus
|
||||
|
||||
Online Analyze : https://pyas-security.com/analyze
|
||||
|
||||
Packaged Download: https://github.com/87owo/PYAS/releases
|
||||
|
||||
## Project License
|
||||
|
||||
For any questions, needs, or bug feedback, please contact us through the following website.
|
||||
|
||||
Source Issues : https://github.com/87owo/PYAS/issues
|
||||
|
||||
Official Email : mailto:service.pyas@gmail.com
|
||||
Official Email : service@pyas-security.com
|
||||
Reference in new issue
Block a user