Skip to content

Commit 5958ea6

Browse files
authored
Merge pull request kevoreilly#2913 from ncatlin/realistic_mouse_movement
Make human.py mouse movement more realistic
2 parents f310633 + fa91d66 commit 5958ea6

File tree

1 file changed

+62
-13
lines changed
  • analyzer/windows/modules/auxiliary

1 file changed

+62
-13
lines changed

analyzer/windows/modules/auxiliary/human.py

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import contextlib
88
import logging
99
import random
10+
import time
11+
import math
1012
import re
1113
import traceback
1214
from ctypes import POINTER, WINFUNCTYPE, byref, c_bool, c_int, create_unicode_buffer, memmove, sizeof, wintypes
@@ -280,6 +282,53 @@ def interact_with_window(hwnd, lparam):
280282
return True
281283

282284

285+
def move_mouse_realistically(dest_x, dest_y):
286+
current_pos = get_cursor_position()
287+
start_x, start_y = current_pos['x'], current_pos['y']
288+
289+
# Calculate distance to determine total duration
290+
distance = math.sqrt((dest_x - start_x)**2 + (dest_y - start_y)**2)
291+
if distance < 1:
292+
return
293+
294+
# 1. Create random control points for a natural curve
295+
# These points 'pull' the path away from a perfect straight line
296+
# Offsetting these creates a unique arc every time
297+
offset = distance * 0.15
298+
ctrl_x1 = start_x + (dest_x - start_x) * 0.3 + random.uniform(-offset, offset)
299+
ctrl_y1 = start_y + (dest_y - start_y) * 0.3 + random.uniform(-offset, offset)
300+
ctrl_x2 = start_x + (dest_x - start_x) * 0.7 + random.uniform(-offset, offset)
301+
ctrl_y2 = start_y + (dest_y - start_y) * 0.7 + random.uniform(-offset, offset)
302+
303+
steps = random.randint(30, 50) if distance < 500 else random.randint(60, 90)
304+
305+
for i in range(steps + 1):
306+
t = i / steps
307+
308+
# Cubic Bezier Formula for smooth pathing
309+
curr_x = int((1-t)**3 * start_x + 3*(1-t)**2 * t * ctrl_x1 +
310+
3*(1-t) * t**2 * ctrl_x2 + t**3 * dest_x)
311+
curr_y = int((1-t)**3 * start_y + 3*(1-t)**2 * t * ctrl_y1 +
312+
3*(1-t) * t**2 * ctrl_y2 + t**3 * dest_y)
313+
314+
USER32.SetCursorPos(curr_x, curr_y)
315+
316+
# 3. Speed Randomization (The "Human" Factor)
317+
# Base sleep + Sinusoidal deceleration + Gaussian noise (jitter)
318+
base_sleep = 0.001
319+
deceleration = (math.sin(t * math.pi) * 0.005) # Slower at start/end
320+
jitter = random.uniform(0, 0.003) # Tiny random pauses
321+
322+
# Occasional "Micro-hesitation" (1% chance to pause for a frame)
323+
if random.random() < 0.01:
324+
time.sleep(random.uniform(0.01, 0.03))
325+
326+
time.sleep(base_sleep + deceleration + jitter)
327+
328+
# Final snap to ensure precision
329+
USER32.SetCursorPos(dest_x, dest_y)
330+
331+
283332
# Callback procedure invoked for every enumerated window.
284333
def handle_window_interaction(hwnd, lparam):
285334
# we also want to inspect the "parent" windows, not just the children
@@ -295,7 +344,7 @@ def get_window_list(hwnd, lparam):
295344
return True
296345

297346

298-
def move_mouse():
347+
def move_mouse_random():
299348
# To avoid mousing over desktop icons, use 1/4 of the total resolution as the starting pixel
300349
x = random.randint(RESOLUTION_WITHOUT_TASKBAR["x"] // 4, RESOLUTION_WITHOUT_TASKBAR["x"])
301350
y = random.randint(0, RESOLUTION_WITHOUT_TASKBAR["y"])
@@ -306,7 +355,7 @@ def move_mouse():
306355
# the mouse events. This actually moves the cursor around which might
307356
# cause some unintended activity on the desktop. We might want to make
308357
# this featur optional.
309-
USER32.SetCursorPos(x, y)
358+
move_mouse_realistically(x, y)
310359

311360

312361
def click_mouse():
@@ -320,7 +369,7 @@ def click_mouse():
320369
def click_around(hwnd, workable_range_x, workable_range_y):
321370
USER32.SetForegroundWindow(hwnd)
322371
# first click the middle
323-
USER32.SetCursorPos(RESOLUTION["x"] // 2, RESOLUTION["y"] // 2)
372+
move_mouse_realistically(RESOLUTION["x"] // 2, RESOLUTION["y"] // 2)
324373
click_mouse()
325374
KERNEL32.Sleep(50)
326375
click_mouse()
@@ -332,23 +381,23 @@ def click_around(hwnd, workable_range_x, workable_range_y):
332381
# make sure the window still exists
333382
if USER32.IsWindowVisible(hwnd):
334383
USER32.SetForegroundWindow(hwnd)
335-
USER32.SetCursorPos(x, RESOLUTION["y"] // 2)
384+
move_mouse_realistically(x, RESOLUTION["y"] // 2)
336385
click_mouse()
337386
KERNEL32.Sleep(50)
338387
click_mouse()
339388
KERNEL32.Sleep(50)
340389
if not USER32.IsWindowVisible(hwnd):
341390
break
342391
USER32.SetForegroundWindow(hwnd)
343-
USER32.SetCursorPos(x, random.randint(workable_range_y[0], workable_range_y[1]))
392+
move_mouse_realistically(x, random.randint(workable_range_y[0], workable_range_y[1]))
344393
click_mouse()
345394
KERNEL32.Sleep(50)
346395
click_mouse()
347396
KERNEL32.Sleep(50)
348397
if not USER32.IsWindowVisible(hwnd):
349398
break
350399
USER32.SetForegroundWindow(hwnd)
351-
USER32.SetCursorPos(x, random.randint(workable_range_y[0], workable_range_y[1]))
400+
move_mouse_realistically(x, random.randint(workable_range_y[0], workable_range_y[1]))
352401
click_mouse()
353402
KERNEL32.Sleep(50)
354403
click_mouse()
@@ -368,7 +417,7 @@ def click_around(hwnd, workable_range_x, workable_range_y):
368417
else:
369418
point = re.match(CURSOR_POSITION_REGEX, instruction)
370419
if point and len(point.regs) == 3:
371-
USER32.SetCursorPos(int(point.group(1)), int(point.group(2)))
420+
move_mouse_realistically(int(point.group(1)), int(point.group(2)))
372421
KERNEL32.Sleep(50)
373422
else:
374423
log.info("Breaking out of document window click loop as our window went away")
@@ -459,7 +508,7 @@ def realistic_human_cursor_movement():
459508
counter += RESOLUTION_WITHOUT_TASKBAR["x"] // 64
460509
x = floor(max(0, min(start_x + counter + +fuzzy_x, RESOLUTION_WITHOUT_TASKBAR["x"])))
461510
y = floor(start_y)
462-
USER32.SetCursorPos(x, y)
511+
move_mouse_realistically(x, y)
463512
KERNEL32.Sleep(50)
464513

465514

@@ -554,7 +603,7 @@ def run(self):
554603
return
555604
match = re.match(CURSOR_POSITION_REGEX, instruction, flags=re.IGNORECASE)
556605
if match and len(match.regs) == 3:
557-
USER32.SetCursorPos(int(match.group(1)), int(match.group(2)))
606+
move_mouse_realistically(int(match.group(1)), int(match.group(2)))
558607
KERNEL32.Sleep(interval)
559608
continue
560609
match = re.match(WAIT_REGEX, instruction, flags=re.IGNORECASE)
@@ -576,24 +625,24 @@ def run(self):
576625
rng = random.randint(0, 7)
577626
if rng > 1: # 0-1
578627
if rng < 4: # 2-3 25% of the time move the cursor on the middle of the screen for x and move around
579-
USER32.SetCursorPos(RESOLUTION["x"] // 2, 0)
628+
move_mouse_realistically(RESOLUTION["x"] // 2, 0)
580629
# Avoid clicking on console windows and suspending execution
581630
if not cursor_over_console_window():
582631
click_mouse()
583-
move_mouse()
632+
move_mouse_random()
584633
elif (
585634
rng >= 6
586635
): # 6-7 25% of the time do realistic human movements for things like https://thehackernews.com/2023/11/lummac2-malware-deploys-new.html
587636
realistic_human_cursor_movement()
588637
else: # 4-5 25% of the time move the cursor somewhere random and click/move around
589-
USER32.SetCursorPos(
638+
move_mouse_realistically(
590639
int(RESOLUTION_WITHOUT_TASKBAR["x"] / random.uniform(1, 16)),
591640
int(RESOLUTION_WITHOUT_TASKBAR["y"] / random.uniform(1, 16)),
592641
)
593642
# Avoid clicking on console windows and suspending execution
594643
if not cursor_over_console_window():
595644
click_mouse()
596-
move_mouse()
645+
move_mouse_random()
597646

598647
if (seconds % (15 + randoff)) == 0:
599648
# curwind = USER32.GetForegroundWindow()

0 commit comments

Comments
 (0)