From b22b3e900be883d6e05111743c3d303ed85212cd Mon Sep 17 00:00:00 2001 From: Josh Feather <142008135+josh-feather@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:08:33 +0100 Subject: [PATCH] Add checkbox, radio button compatibility to Human aux module (#2321) Improves the likelihood of a successful multi-stage detonation when the sample requires human interaction before delivering additional payloads. --- analyzer/windows/lib/common/defines.py | 41 ++- analyzer/windows/modules/auxiliary/human.py | 340 ++++++++++++-------- 2 files changed, 229 insertions(+), 152 deletions(-) diff --git a/analyzer/windows/lib/common/defines.py b/analyzer/windows/lib/common/defines.py index 6b9de229af4..b14b1de0839 100644 --- a/analyzer/windows/lib/common/defines.py +++ b/analyzer/windows/lib/common/defines.py @@ -2,9 +2,9 @@ # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org # See the file 'docs/LICENSE' for copying permission. +import sys from ctypes import ( POINTER, - WINFUNCTYPE, Structure, Union, c_bool, @@ -17,16 +17,21 @@ c_ushort, c_void_p, c_wchar_p, - windll, ) - -NTDLL = windll.ntdll -KERNEL32 = windll.kernel32 -ADVAPI32 = windll.advapi32 -USER32 = windll.user32 -SHELL32 = windll.shell32 -PDH = windll.pdh -PSAPI = windll.psapi +if sys.platform == "win32": + from ctypes import ( + WINFUNCTYPE, + windll, + ) + NTDLL = windll.ntdll + KERNEL32 = windll.kernel32 + ADVAPI32 = windll.advapi32 + USER32 = windll.user32 + SHELL32 = windll.shell32 + PDH = windll.pdh + PSAPI = windll.psapi + EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) + EnumChildProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) BYTE = c_ubyte USHORT = c_ushort @@ -96,6 +101,7 @@ ERROR_BROKEN_PIPE = 0x0000006D ERROR_MORE_DATA = 0x000000EA ERROR_PIPE_CONNECTED = 0x00000217 +ERROR_INVALID_HANDLE = 0x00000006 WAIT_TIMEOUT = 0x00000102 @@ -137,6 +143,17 @@ MAX_PATH = 260 +# Button messages +BM_SETCHECK = 0x000000F1 +BM_GETCHECK = 0x000000F0 +# Button states +BST_UNCHECKED = 0x0000 +BST_CHECKED = 0x0001 +BST_INDETERMINATE = 0x0002 + +# Process cannot access the file because it is being used by another process. +ERROR_SHARING_VIOLATION = 0x00000020 + class STARTUPINFO(Structure): _fields_ = [ @@ -311,7 +328,3 @@ class PDH_FMT_COUNTERVALUE(Structure): ("CStatus", DWORD), ("doubleValue", DOUBLE), ] - - -EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) -EnumChildProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) diff --git a/analyzer/windows/modules/auxiliary/human.py b/analyzer/windows/modules/auxiliary/human.py index 2654c0e33f2..eac37fb9a16 100644 --- a/analyzer/windows/modules/auxiliary/human.py +++ b/analyzer/windows/modules/auxiliary/human.py @@ -15,14 +15,25 @@ from threading import Thread from lib.common.abstracts import Auxiliary -from lib.common.defines import BM_CLICK, CF_TEXT, GMEM_MOVEABLE, KERNEL32, USER32, WM_CLOSE, WM_GETTEXT, WM_GETTEXTLENGTH +from lib.common.defines import ( + BM_CLICK, + BM_GETCHECK, + BM_SETCHECK, + BST_CHECKED, + CF_TEXT, + GMEM_MOVEABLE, + KERNEL32, + USER32, + WM_CLOSE, + WM_GETTEXT, + WM_GETTEXTLENGTH, +) log = logging.getLogger(__name__) EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) EnumChildProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) - CURSOR_POSITION_REGEX = r"\((\d+):(\d+)\)" WAIT_REGEX = r"WAIT(\d+)" INTERVAL_REGEX = r"INTERVAL(\d+)" @@ -46,152 +57,205 @@ CLOSED_DOCUMENT_WINDOW = False DOCUMENT_WINDOW_CLICK_AROUND = False - -def queryMousePosition(): +CLICK_BUTTONS = ( + # english + "yes", + "ok", + "accept", + "next", + "install", + "run", + "agree", + "enable", + "retry", + "don't send", + "don't save", + "continue", + "unzip", + "open", + "close the program", + "save", + "later", + "finish", + "end", + "keep", + "allow access", + "remind me later", + # german + "ja", + "weiter", + "akzeptieren", + "ende", + "starten", + "jetzt starten", + "neustarten", + "neu starten", + "jetzt neu starten", + "beenden", + "oeffnen", + "schliessen", + "installation weiterfuhren", + "fertig", + "beenden", + "fortsetzen", + "fortfahren", + "stimme zu", + "zustimmen", + "senden", + "nicht senden", + "speichern", + "nicht speichern", + "ausfuehren", + "spaeter", + "einverstanden", + # ru + "установить", +) + +DONT_CLICK_BUTTONS = ( + # english + "check online for a solution", + "don't run", + "do not ask again until the next update is available", + "cancel", + "do not accept the agreement", + "i would like to help make reader even better", + "restart now", + # german + "abbrechen", + "online nach losung suchen", + "abbruch", + "nicht ausfuehren", + "hilfe", + "stimme nicht zu", + # ru + "приoстановить", + "отмена", +) + +OFFICE_WINDOW_CLASSES = ( + "nuidialog", + "bosa_sdm_msword", +) + +def get_cursor_position(): pt = wintypes.POINT() USER32.GetCursorPos(byref(pt)) return {"x": pt.x, "y": pt.y} +def get_window_text(hwnd): + length = USER32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0) + if length == 0: + return "" + text = create_unicode_buffer(length + 1) + USER32.SendMessageW(hwnd, WM_GETTEXT, length + 1, text) + return text.value.replace("&", "") + +def is_button_checked(hwnd): + return bool(USER32.SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED) -def foreach_child(hwnd, lparam): - classname = create_unicode_buffer(128) - USER32.GetClassNameW(hwnd, classname, 128) - - # Check if the class of the child is button. - if ( - "button" in classname.value.lower() - or "button" not in classname.value.lower() - and classname.value in ("NUIDialog", "bosa_sdm_msword") - ): - # Get the text of the button. - length = USER32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0) - if not length: - return True - text = create_unicode_buffer(length + 1) - USER32.SendMessageW(hwnd, WM_GETTEXT, length + 1, text) - textval = text.value.replace("&", "") - if "Microsoft" in textval and classname.value in ("NUIDialog", "bosa_sdm_msword"): - log.info("Issuing keypress on Office dialog") - USER32.SetForegroundWindow(hwnd) - # enter key down/up - USER32.keybd_event(0x0D, 0x1C, 0, 0) - USER32.keybd_event(0x0D, 0x1C, 2, 0) - return False - - # we don't want to bother clicking any non-visible child elements, as they - # generally won't respond and will cause us to fixate on them for the - # rest of the analysis, preventing progress with visible elements - - if not USER32.IsWindowVisible(hwnd): - return True - - # List of buttons labels to click. - buttons = ( - # english - "yes", - "ok", - "accept", - "next", - "install", - "run", - "agree", - "enable", - "retry", - "don't send", - "don't save", - "continue", - "unzip", - "open", - "close the program", - "save", - "later", - "finish", - "end", - "keep", - "allow access", - "remind me later", - # german - "ja", - "weiter", - "akzeptieren", - "ende", - "starten", - "jetzt starten", - "neustarten", - "neu starten", - "jetzt neu starten", - "beenden", - "oeffnen", - "schliessen", - "installation weiterfuhren", - "fertig", - "beenden", - "fortsetzen", - "fortfahren", - "stimme zu", - "zustimmen", - "senden", - "nicht senden", - "speichern", - "nicht speichern", - "ausfuehren", - "spaeter", - "einverstanden", - # ru - "установить", - ) - - # List of buttons labels to not click. - dontclick = ( - # english - "check online for a solution", - "don't run", - "do not ask again until the next update is available", - "cancel", - "do not accept the agreement", - "i would like to help make reader even better", - "restart now", - # german - "abbrechen", - "online nach losung suchen", - "abbruch", - "nicht ausfuehren", - "hilfe", - "stimme nicht zu", - # ru - "приoстановить", - "отмена", - ) - - # Check if the button is set as "clickable" and click it. - for button in buttons: - if button in textval.lower(): - dontclickb = False - for btn in dontclick: - if btn in textval.lower(): - dontclickb = True - if not dontclickb: - log.info('Found button "%s", clicking it' % text.value) - USER32.SetForegroundWindow(hwnd) - KERNEL32.Sleep(1000) - USER32.SendMessageW(hwnd, BM_CLICK, 0, 0) - # only stop searching when we click a button - return False +def send_click(hwnd): + USER32.SetForegroundWindow(hwnd) + KERNEL32.Sleep(1000) + USER32.SendMessageW(hwnd, BM_CLICK, 0, 0) + +def click_button(hwnd, classname): + button_text = get_window_text(hwnd) + if button_text == "": + return True + + if not USER32.IsWindowEnabled(hwnd): + # Ignore buttons that are disabled + return True + + if "Microsoft" in button_text and classname in OFFICE_WINDOW_CLASSES: + log.info("Issuing keypress on Office dialog") + USER32.SetForegroundWindow(hwnd) + # enter key down event + USER32.keybd_event(0x0D, 0x1C, 0, 0) + # enter key up event + USER32.keybd_event(0x0D, 0x1C, 2, 0) + return False + + # Check if the button is set as "clickable" and click it. + button_text = button_text.lower() + + ignore_matches = [text for text in DONT_CLICK_BUTTONS if text in button_text] + if ignore_matches: + return True + + click_matches = [text for text in CLICK_BUTTONS if text in button_text] + if click_matches: + log.info('Found button "%s", clicking it', button_text) + send_click(hwnd) + # only stop searching when we click a button + return False + # continue searching through windows + return True + +def check_button(hwnd): + if is_button_checked(hwnd): + return True + + button_text = get_window_text(hwnd).lower() + + ignore_matches = [text for text in DONT_CLICK_BUTTONS if text in button_text] + if ignore_matches: + return True + + matches = [text for text in CLICK_BUTTONS if text in button_text] + if matches: + log.info('Found checkable button "%s", checking it', button_text) + # try clicking it first + send_click(hwnd) + if not is_button_checked(hwnd): + # if it's still unchecked, check it + USER32.SendMessageW(hwnd, BM_SETCHECK, BST_CHECKED, 0) + # only stop searching when we click a checkbox + return False + # continue searching through windows + return True + +def is_button(classname): + return bool( + "button" in classname + or ("button" not in classname and classname in OFFICE_WINDOW_CLASSES) + ) + +def is_checkbox(classname): + return "checkbox" in classname + +def is_radio_button(classname): + return "radiobutton" in classname + +def interact_with_window(hwnd, lparam): + # we don't want to bother clicking any non-visible child elements, as they + # generally won't respond and will cause us to fixate on them for the + # rest of the analysis, preventing progress with visible elements + if not USER32.IsWindowVisible(hwnd): + return True + + classname_ptr = create_unicode_buffer(128) + USER32.GetClassNameW(hwnd, classname_ptr, 128) + classname = str(classname_ptr.value).lower() + + if is_checkbox(classname) or is_radio_button(classname): + return check_button(hwnd) + elif is_button(classname): + return click_button(hwnd, classname) + # continue searching through windows return True # Callback procedure invoked for every enumerated window. -def foreach_window(hwnd, lparam): - # If the window is visible, enumerate its child objects, looking - # for buttons. - if USER32.IsWindowVisible(hwnd): - # we also want to inspect the "parent" windows, not just the children - foreach_child(hwnd, lparam) - USER32.EnumChildWindows(hwnd, EnumChildProc(foreach_child), 0) +def handle_window_interaction(hwnd, lparam): + # we also want to inspect the "parent" windows, not just the children + interact_with_window(hwnd, lparam) + USER32.EnumChildWindows(hwnd, EnumChildProc(interact_with_window), 0) return True -def getwindowlist(hwnd, lparam): +def get_window_list(hwnd, lparam): global INITIAL_HWNDS if USER32.IsWindowVisible(hwnd): INITIAL_HWNDS.append(hwnd) @@ -436,7 +500,7 @@ def run(self): elif "PDF" in file_type or file_name.endswith(".pdf"): doc = True - USER32.EnumWindows(EnumWindowsProc(getwindowlist), 0) + USER32.EnumWindows(EnumWindowsProc(get_window_list), 0) interval = 300 # Interval of 300 was chosen it looked like human speed try: iter(GIVEN_INSTRUCTIONS) @@ -498,7 +562,7 @@ def run(self): if len(other_hwnds): USER32.SetForegroundWindow(other_hwnds[random.randint(0, len(other_hwnds) - 1)]) - USER32.EnumWindows(EnumWindowsProc(foreach_window), 0) + USER32.EnumWindows(EnumWindowsProc(handle_window_interaction), 0) KERNEL32.Sleep(1000) seconds += 1 except Exception: