Перетаскивание окна за любое место

Перетаскивание окна за любое место, а не только за заголовок, реализуется очень просто, но смотрится очень эффектно. Тем более, что окно может быть вообще без заголовка. Для этого в обработчик окна надо добавить следующий код.

; Процедура обработчика окна
proc  DialogProc hwnddlg,msg,wparam,lparam
      ......
      ; Нажата левая кнопка мышки на окне?
      cmp     [msg], WM_LBUTTONDOWN
      je      drag_window
      ......
drag_window:
      ; Освободить захват мыши окном в текущем потоке и
      ; восстановить обычную обработку ввода данных от мыши
      invoke  ReleaseCapture
      ; Перенаправить сообщение передвижения мышью SC_MOVE на заголовок окна
      ; 61458 = SC_MOVE or HTCAPTION, в FASM по умолчанию не определено,
      ; поэтому сразу задается числовым значением
      invoke  SendMessage,[hwnddlg],WM_SYSCOMMAND,61458,0
      ......

Вызов функции ReleaseCapture необходим для освобождения окна от захвата сообщений мыши. Даже если вы сами не устанавливали эти перехватчики, то они вполне могут быть установлены сторонними программами. В этом случае окно может перемещаться неправильно. Исходник с откомпилированным файлом прилагается.

Добавлено: 10 Апреля 2018 20:48:11 Добавил: Андрей Ковальчук

Создание прозрачных окон в Windows

Еще один простой в реализации, но красивый эффект для ваших программ - прозрачные диалоговые окна. Для этого надо, чтобы диалоговое окно было прописано в ресурсах или создавалось с расширенным стилем WS_EX_LAYERED, а при его инициализации вызывалась функция SetLayeredWindowAttributes. Значение коэффициента прозрачности может быть от 0 (полностью прозрачное окно) до 255 (непрозрачное окно). Рекомендуется значение 240-245, при этом эффект прозрачности уже заметен, а содержимое окна еще легко читается и не сливается с перекрываемыми окнами. Этой же функцией можно динамически менять прозрачность уже созданного окна, например чтобы создать эффект его плавного появления или исчезновения.

; Сегмент кода
section '.code' code readable executable
...
; Процедура обработчика окна
proc DialogProc hwnddlg,msg,wparam,lparam
        ...
        ; Инициализация окна?
        cmp     [msg], WM_INITDIALOG
        je      wminitdialog
        ; Закрытие окна?
        cmp     [msg], WM_CLOSE
        je      wmclose
        ...
wminitdialog:
        ; Установить первоначальную прозрачность окна 245
        invoke  SetLayeredWindowAttributes, [hwnddlg], 0, 245, LWA_ALPHA
        ...
wmclose:
        ; Плавное исчезновение окна при его закрытии
        mov     ecx,245
fade_dialog:
        push    ecx
        ; Установка нового атрибта прозрачности
        invoke  SetLayeredWindowAttributes, [hwnddlg], 0, ecx, LWA_ALPHA
        ; Небольшая пауза
        invoke  Sleep,2
        pop     ecx
        loop    fade_dialog
        ...
 
; Секция ресурсов
section '.rsrc' resource data readable
 
; Описание диалогового окна в ресурсах.
; В расширенных стилях должен быть прописан атрибут "WS_EX_LAYERED"
dialog demo, 'Demo', 70, 70, 190, 175,\
        WS_CAPTION + WS_POPUP + WS_SYSMENU + DS_MODALFRAME,\
        WS_EX_LAYERED
        ...

Не забывайте вызывать функцию SetLayeredWindowAttributes при инициализации диалогового окна со стилем WS_EX_LAYERED, иначе после открытия его вообще не будет видно. Также помните, что эффект прозрачности не поддерживается в Windows 9x. Пример программы с плавным появлением и исчезновением окна прилагается.

Добавлено: 10 Апреля 2018 20:47:13 Добавил: Андрей Ковальчук

Обработка подключения и отключения съемного накопителя

С расширением рынка переносных устройств и USB-накопителей становится актуальной задача по обработке их взаимодействия с компьютером. Сегодня разберем обработку подключения и отключения съемных накопителей, таких как Flash-диски, карты памяти и USB-диски. Начинаем с теории. При подключении или отключении съемного накопителя система посылает всем окнам (через глобальный хэндл HWND_BROADCAST) сообщение WM_DEVICECHANGE. Но это сообщение всего лишь о самом факте изменения состояния съемного накопителя, а более подробные значения содержатся в параметрах lParam и wParam этого сообщения. В wParam приходит расшифровка произошедшего события: подключение, отключение, изменения состояния, отмена отключения и т.п. Нас пока интересует только два: DBT_DEVICEARRIVAL - подключение сменного накопителя и DBT_DEVICEREMOVECOMPLETE - извлечение накопителя. Основной обработчик событий приложения ничем не отличается от обычных обработчиков. Нам также понадобятся несколько констант, которые по умолчанию не определены в FASM:

DBT_DEVICEARRIVAL        = 0x8000
DBT_DEVICEREMOVECOMPLETE = 0x8004
 
DBT_DEVTYP_VOLUME        = 0x00000002
И, собственно, сам обработчик. Я оставил только нужные фрагменты кода:
Code (Assembler) : Убрать нумерацию
proc DialogProc hwnddlg,msg,wparam,lparam
        push    ebx esi edi
        ...
        ; Пришло сообщение об изменении состояния съемного накопителя
        cmp     [msg],WM_DEVICECHANGE
        je      update_usb
        ...
update_usb:
        ; Устройство подключено?
        cmp     [wparam],DBT_DEVICEARRIVAL
        je      usb_connected
 
        ; Устройство извлечено?
        cmp     [wparam],DBT_DEVICEREMOVECOMPLETE
        je      usb_disconnected
 
        jmp     processed
 
usb_connected:
        ; Обработка подключения устройства
        ...
        jmp     processed
 
usb_disconnected:
        ; Обработка отключения устройства
        ...
        jmp     processed
        ...
processed:
        mov     eax,1
finish:
        pop     edi esi ebx
        ret
endp

Для полноты картины осталось узнать, какой именно диск был подключен или извлечен. В параметре lParam сообщения WM_DEVICECHANGE передается указатель на структуру, которая содержит подробную информацию о случившемся событии. FASM про нее тоже ничего не знает, поэтому придется и ее описать самостоятельно.

Структура состоит из двух частей: заголовка и информационной части. В заголовке содержится общая информация, а информационная часть может различаться в зависимости от типа устройства, на котором произошло событие. Для съемных дисков она будет следующая:
struct DEV_BROADCAST
        ; Заголовок структуры
        dbch_size       dd ?
        dbch_devicetype dd ?
        dbch_reserved   dd ?
 
        ; Информационная часть структуры
        dbcv_unitmask   dd ?
        dbcv_flags      dd ?
ends

Поскольку сообщения могут приходить от разных устройств, а нас интересуют только съемные накопители, то в обработчики придется добавить фильтр. Поле заголовка dbch_devicetypeсодержит значение типа устройства, в нашем случае это должно быть DBT_DEVTYP_VOLUME, оно описано выше. Все остальные сообщения мы должны пропускать без обработки. Также имейте в виду, что некоторые устройства, например, флоппи-дисководы или CD-ROM могут вообще не инициировать никаких событий, и смену носителей в них отследить не получится.

Чтобы узнать букву диска, которую система присвоила съемному носителю, надо разобрать значение dbcv_unitmask из информационной части структуры. Это битовая маска, где каждый отдельный бит соответствует определенной букве: бит 0 соответствует диску A, бит 1 - диску B, бит 2 - диску C и так далее. Я нашел несколько вариантов преобразования такой маски в человеко-понятную букву, но решил сделать свой, обойдясь двумя ассемблерными командами:
        ; Получить букву диска из битовой маски (маска в регистре EAX)
        bsr     eax,eax
        ; В регистре AL буква диска
        add     al,'A'

Теперь у нас есть все данные для обработки подключения и отключения съемных накопителей. Вот некоторые варианты применения таких обработчиков: резервное сохранение важных данных при неожиданном отключении накопителя, антивирусный мониторинг подключаемых USB-дисков, незаметное фоновое копирование данных с флешек или запись на них какой-либо информации, и многое-многое другое. Все зависит только от вашей фантазии и порядочности.

В приложении пример программы, записывающей в свой лог все сообщения о подключении и отключении съемных носителей с указанием букв дисков.

Добавлено: 10 Апреля 2018 20:46:33 Добавил: Андрей Ковальчук

Создание диалоговых окон с тенью

Сегодня разберем очередное украшательство для ваших программ, а именно тень от диалоговых окон. Обычная тень создается штатными средствами системы, но поддерживается только начиная с Windows XP. Для этого требуется, чтобы стиль окна включал в себя флаг CS_DROPSHADOW, и здесь есть одна тонкость: этот флаг нельзя прописать в ресурсах, а надо устанавливать при инициализации диалогового окна. В обработчике инициализации должен быть такой код:

        ...
        ; Определить константу CS_DROPSHADOW
        CS_DROPSHADOW = 00020000h
        ; Получить текущее значение стиля окна
        invoke  GetWindowLong,[hwnddlg],GCL_STYLE
        ; Добавить к нему атрибут тень
        or      eax,CS_DROPSHADOW
        ; Установить новый стиль окна
        invoke  SetClassLong,[hwnddlg],GCL_STYLE,eax   
        ...

Тут есть плюсы: корректно поддерживаются окна любой нестандартной и сложной формы, а все следующие окна процесса автоматически создаются сразу с тенью. Но есть один странный момент. Несмотря на то, что дочерние окна сперва создаются с корректной тенью, при любом перемещении их над родительским окном тени родительского и дочернего окна сливаются в одну.

Похоже это связано с тем, что родительское окно и все его дочерние окна считаются как один объект. А тень по сути является одним самостоятельным окном, которое и связывается с этим объектом. Для обхода такого недоразумения придется создавать псевдо-дочерние окна, которые находятся на одном уровне иерархии с главным окном. А чтобы они вели себя как дочерние, главное окно надо будет блокировать до их закрытия. Вот фрагмент кода открытия псевдо-дочернего окна сообщения или диалогового окна:
        ...
        ; Заблокировать главное окно
        invoke  EnableWindow,[hwnddlg],FALSE
 
        ; Открыть псевдо-дочернее окно с тенью, его родитель по уровню
        ; иерархии равен родителю главного окна. В результате этого при
        ; его перемещении над главным окном тень теряться не будет
        invoke  MessageBox,NULL,szMess,szTitle,MB_OK
        ; или...
        invoke  DialogBoxParam,[hInstance],ID_DLG,NULL,DialogProc,0
        ; Подразумевается, что родительское окно создано с hWndParent = NULL
 
        ; Разблокировать главное окно и вернуть на него фокус
        invoke  EnableWindow,[hwnddlg],TRUE
        invoke  SetFocus,[hwnddlg]  
        ...

При такой реализации открытия окон поведение тени становится полностью корректным, при любом перемещении окон тень не теряется и не сливается.

В приложении два примера создания окон с тенью. В первом случае создается реальное дочернее окно, а во втором примере - псевдо-дочернее окно с блокировкой главного окна.

Добавлено: 10 Апреля 2018 20:45:37 Добавил: Андрей Ковальчук

Обработка перетаскивания файлов (Drag'n'Drop)

Если в вашем приложении используется обработка файлов, то кроме открытия через стандартные диалоги выбора файла и каталога, можно получать их из Проводника Windows перетаскиванием. Обработка перетаскивания файлов выполняется в два этапа. При инициализации диалогового окна приложения должна вызываться функция DragAcceptFiles. Параметр функции TRUE разрешает принятие файлов окном, а FALSE его запрещает, так что прием файлов можно регулировать динамически. Непосредственно прием файлов окном выполняется функцией DragQueryFile.

; Сегмент кода
section '.code' code readable executable
        ... 
; Процедура обработчика окна
proc DialogProc hwnddlg,msg,wparam,lparam 
        ...
        ; Инициализация окна
        cmp     [msg],WM_INITDIALOG
        je      wminitdialog
        ; Обработка перетаскивания файлов
        cmp     [msg],WM_DROPFILES
        je      wmdropfiles
        ...
wminitdialog:
        ; Разрешить окну принимать файлы
        invoke  DragAcceptFiles,[hwnddlg],TRUE
        jmp     processed
 
wmdropfiles:
        ; Обработка полученных файлов. Функция DragQueryFile возвращает имя
        ; файла с указанным индексом (нумерация индексов начинается с нуля).
        ; Для получения общего количества переданных файлов ее надо вызвать с
        ; индексом равным 0FFFFFFFFh
        invoke  DragQueryFile,[wparam],0FFFFFFFFh,NULL,NULL
        ; В регистре EAX количество переданных файлов
 
        ; Перебрать по очереди все переданные окну файлы
        xor     ecx,ecx
process_file:
        push    ecx eax
        ; Получить имя файла или каталога в буфер fname
        invoke  DragQueryFile,[wparam],ecx,fname,100h
        ...
        ; Тут будет обработчик переданных файлов и каталогов
        ...
        pop     eax ecx
        inc     ecx
        cmp     ecx,eax
        jne     process_file
        ...

Если требуется получить только один файл, то запрашивать общее количество файлов не обязательно, достаточно вызвать DragQueryFile с нулевым индексом файла. Исходники двух вариантов обработчиков с откомпилированными файлами прилагаются.

Добавлено: 10 Апреля 2018 20:44:49 Добавил: Андрей Ковальчук

Окно поверх всех окон (Always On Top)

Установка окна поверх всех других окон бывает удобна, когда надо привлечь внимание пользователя к важной информации, или окно с какими-либо данными должно находиться всегда перед глазами. Такое свойство окна можно прописать сразу в ресурсах, а можно изменять динамически, например при нажатии на кнопку-чекбокс "Always on top" или при изменении каких-нибудь внутренних настроек вашей программы.

Для создания окна поверх всех других окон, оно должно быть описано в ресурсах с флагом DS_SYSMODAL. Для динамического изменения используется функция SetWindowPos с флагами SWP_NOMOVE и SWP_NOSIZE. Флаги нужны для того, чтобы не изменять размеры и положение окна.

; Идентификатор чекбокса в ресурсах
ID_ONTOP        = 101
 
; Сегмент кода
section '.code' code readable executable
        ...
; Процедура обработчика окна
proc DialogProc hwnddlg,msg,wparam,lparam
        ; Обработка нажатия на кнопку-чекбокс
        cmp     [wparam],BN_CLICKED shl 16 + ID_ONTOP
        je      .ontop
        ...
.ontop:
        ; Получить состояние чекбокса
        invoke  IsDlgButtonChecked,[hwnddlg],ID_ONTOP
        cmp     eax,BST_CHECKED
        ; По умолчанию будем считать что галочка поставлена
        mov     eax,HWND_TOPMOST
        je      @f
        ; Галочка не поставлена, убрать атрибут "поверх всех окон"
        mov     eax,HWND_NOTOPMOST
@@:
        ; Установить параметр окна "поверх всех окон", изменение размера
        ; и положения окна не производится, это установлено флагами
        invoke  SetWindowPos,[hwnddlg],eax,0,0,0,0,SWP_NOMOVE+SWP_NOSIZE
        jmp     .processed
        ...
 
; Секция ресурсов
section '.rsrc' resource data readable
; Диалог описан со стилем DS_SYSMODAL - поверх всех окон
dialog demonstration, 'Always on top Demo', 0, 0, 190, 55,\
        WS_CAPTION+WS_SYSMENU+DS_CENTER+DS_SYSMODAL
        ...
        ; Кнопка-чекбокс, которая будет управлять положением окна
        dialogitem 'BUTTON','Always on top', ID_ONTOP, 5, 150, 63, 13,\
        WS_VISIBLE+BS_AUTOCHECKBOX+BS_FLAT
        ...

В приведенном примере при поставленной галочке "Always on top" окно будет находиться поверх других окон. Исходник с откомпилированным файлом прилагается.

Добавлено: 10 Апреля 2018 20:44:09 Добавил: Андрей Ковальчук

Вывод лога на Ассемблере

Если при работе вашей программы требуется вывод лога неопределенной длины, то это удобно делать в многострочном поле Edit. Преимущества такого способа налицо: можно мышкой выделить и скопировать любой кусок текста, средствами API получить весь текст целиком и сохранить в файл, быстро очистить окно лога и многое другое. Удобно делать все, кроме главного: добавление новых строчек в лог. Конечно, можно получать текст, в памяти добавлять к нему новые строчки и вставлять обратно. Но это долго, неудобно и требует дополнительных ресурсов, а для очень больших логов и вовсе неприемлемо. Поэтому воспользуемся следующей функцией:

;---------------------------------------------
; procedure AddLog
; void AddLog(hWnd:dword,CtrlID:dword,pStr:&string)
;---------------------------------------------
proc    AddLog  hWnd:dword,CtrlID:dword,pStr:dword
        push    eax
        invoke  GetDlgItem,[hWnd],[CtrlID]
        or      eax,eax
        jz      .AddLog_1
        mov     [CtrlID],eax
        invoke  SendMessage,[CtrlID],EM_GETLINECOUNT,0,0
        dec     eax
        invoke  SendMessage,[CtrlID],EM_LINEINDEX,eax,0
        invoke  SendMessage,[CtrlID],EM_SETSEL,eax,eax
        invoke  SendMessage,[CtrlID],EM_REPLACESEL,FALSE,[pStr]
.AddLog_1:
        pop     eax
        ret
endp

Парметры вызова: hWnd - хэндл окна, которому принадлежит дочернее окно логом, CtrlID - идентификатор окна Edit в ресурсах, pStr - указатель на строку ASCIIZ, которую надо записать в лог.

Пример использования:
; Идентификатор окна лога в ресурсах
ID_LOG  = 100  
 
; Сегмент данных
section '.data' data readable writeable
...
; Строчки для добавления в лог
line1   db 'Some action 1',13,10,0
line2   db 'Some action 2',13,10,0
line3   db 'Some action 3',13,10,0
 
; Сегмент кода
section '.code' code readable executable
...
        ; Средствами API записать первую строчку в лог
        invoke  SetDlgItemText,[hwnddlg],ID_LOG,line1
 
        ; Дописать к ней две других строки
        stdcall AddLog,[hwnddlg],ID_LOG,line2
        stdcall AddLog,[hwnddlg],ID_LOG,line3
...
 
; Ресурсы
section '.rsrc' resource data readable
...
dialogitem 'EDIT','', ID_LOG, 10,10,100,200, WS_VISIBLE + ES_AUTOVSCROLL +\   
            ES_MULTILINE + WS_BORDER + ES_READONLY
...

Окно лога желательно делать недоступным для редактирования (со стилем ES_READONLY). Строчки для добавления в лог обязательно должны заканчиваться символами перевода строки CRLF и нулем.

Добавлено: 10 Апреля 2018 20:43:38 Добавил: Андрей Ковальчук

Повышение привилегий процесса

Любой процесс в системе выполняется с правами какого-то пользователя или самой системы. Привилегии – это права процесса на совершение каких-либо действий по отношению ко всей системе, и при выполнении каких-либо привилегированных операций система проверяет, обладает ли пользователь соответствующей привилегией. Например, выключение и перезагрузка компьютера компьютера относятся как раз к таким операциям, и без повышения привилегий функция ExitWindowsEx завершится с ошибкой. Готовых решений на FASM найти не удалось, пришлось портировать из языков высокого уровня.

; Сегмент данных
section '.data' data readable writeable
 
; Определяем константы
TOKEN_ADJUST_PRIVILEGES = 20h
TOKEN_QUERY             = 8h
SE_PRIVILEGE_ENABLED    = 2h
 
; Определяем необходимые структуры, потому что в FASM'е их нет
struct LUID
  lowPart  dd ?
  HighPart dd ?
ends
 
struct LUID_AND_ATTRIBUTES
  pLuid       LUID
  Attributes  dd ?
ends
 
struct _TOKEN_PRIVILEGES
  PrivilegeCount   dd ?
  Privileges       LUID_AND_ATTRIBUTES
ends
 
TTokenHd dd ?
 
udtLUID  LUID
tkp     _TOKEN_PRIVILEGES
 
SE_SHUTDOWN_NAME db 'SeShutdownPrivilege',0
 
; Сегмент кода
section '.code' code readable executable
 
    invoke    GetCurrentProcess
 
    ; Открыть маркер доступа (access token), ассоциирующийся с процессом
    invoke    OpenProcessToken,eax,TOKEN_ADJUST_PRIVILEGES+TOKEN_QUERY,TTokenHd
    or        eax,eax
    jz        loc_exit  ; Ошибка
 
    ; Получить текущее значение привилегии на выключение и
    ; перезагрузку системы
    invoke    LookupPrivilegeValue, NULL, SE_SHUTDOWN_NAME, udtLUID
    or        eax,eax
    jz        loc_exit  ; Ошибка
 
    ; Заполнить структуры
    mov       [tkp.PrivilegeCount],1
    mov       [tkp.Privileges.Attributes],SE_PRIVILEGE_ENABLED
    mov       eax,[udtLUID.lowPart]
    mov       [tkp.Privileges.pLuid.lowPart],eax
    mov       eax,[udtLUID.HighPart]
    mov       [tkp.Privileges.pLuid.HighPart],eax
    invoke    AdjustTokenPrivileges,[TTokenHd],0,tkp,0,0,0
 
    ; Здесь будет код, требующий повышенных привилегий, 
    ; например выключение компьютера
    invoke    ExitWindowsEx,EWX_POWEROFF,NULL
    ...
 
    ; Выход
loc_exit:
    invoke    ExitProcess,0

Выключение рассмотрено как наиболее часто встречающаяся задача, более подробное описание использованных структур и функций вы можете найти на сайте Microsoft Developer Network.

Добавлено: 10 Апреля 2018 20:42:59 Добавил: Андрей Ковальчук

Определение времени бездействия системы

Иногда приложениям требуется узнать время бездействия системы, то есть интервал времени, прошедший с момента когда пользователь последний раз пошевелил мышкой или нажал какую-нибудь кнопку на клавиатуре. Для определения время бездействия системы в системах Windows 2000 и старше используется функция API GetLastInputInfo. Она возвращает количество миллисекунд (тиков таймера), прошедшее от старта системы до момента последнего ввода. Время бездействия вычисляется как арифметическая разница между данными, возвращаемыми функцией GetTickCount и данными из GetLastInputInfo. В FASM, как обычно, ничего из нужных структур не определено, лезем в MSDN:

section '.data' data readable writeable
 
struct  LASTINPUTINFO
        cbSize   dd ?   ; Размер структуры
        dwTime   dd ?   ; Время бездействия
ends
 
lii     LASTINPUTINFO  
Получение времени бездействия системы:
Code (Assembler) : Убрать нумерацию
        ...
        ; Получить время последнего ввода
        mov     [lii.cbSize],sizeof.LASTINPUTINFO
        invoke  GetLastInputInfo,lii
 
        ; Получить текущее время в миллисекундах
        invoke  GetTickCount
 
        ; EAX - время бездействия системы в миллисекундах
        sub     eax,[lii.dwTime]
        ...

Это был самый простой способ, работающий на всех новых системах. В старых операционках типа Windows 9x функция GetLastInputInfo отсутствует, поэтому там придется использовать другой, более громоздкий способ с применением глобальных системных хуков.

Чтобы перехват получился глобальным, придется загружать в память DLL с нужными функциями и устанавливать хуки уже на нее. Хуки будем ставить двух типов: через WH_MOUSE на мышь и через WH_KEYBOARD на клавиатуру. При наступлении любого из этих событий в переменную, находящуюся в shared-памяти DLL, будет записываться значение из функции GetTickCount, а при вызове какой-нибудь заранее определенной функции, это значение будет передаваться нашему приложению. Полный текст DLL я тут приводить не буду, можете посмотреть его, скачав прилагаемый файл с исходниками. В основное приложение DLL подгружается через импорт, при инициализации DLL ставятся глобальные хуки, а при выходе из приложения хуки должны обязательно сниматься.
        ...
        ; Получить время последнего ввода
        invoke  GetLastInputTime
        mov     ebx,eax
 
        ; Получить текущее время в миллисекундах
        invoke  GetTickCount
 
        ; EAX - время бездействия системы в миллисекундах
        sub     eax,ebx
        ...
        ...
        ...
.wmclose:
        ; Снять глобальные хуки
        invoke  UnloadDll
        ...

Этот метод универсальный и работает на всех системах. Но приходится таскать за собой дополнительную DLL, к установке глобальных хуков некоторые "как-бы-антивирусы" и мониторы могут отнестись с подозрением, ну и вдобавок получается много лишнего кода. Выбор оставляю за вами.

В приложении две программы, реализующие оба описанных выше метода. В диалоговом окне они показывают счетчик секунд, прошедших с момента последней активности пользователя. Чтобы сбросить счетчик пошевелите мышкой или нажмите любую клавишу.

Добавлено: 10 Апреля 2018 20:42:13 Добавил: Андрей Ковальчук

Построение карты памяти процесса

Умение работать с памятью сторонних процессов - полезный навык адепта Темной стороны Силы. Таким способом можно найти нужные данные, хранящиеся в памяти, например, расшифрованные файлы, изображения, пароли, и еще много чего вкусного. Также этот навык пригодится при создании распаковщиков, лоадеров для программ и трейнеров для различных игр. Я привел лишь несколько примеров, на практике их можно найти гораздо больше.

Для построения карты памяти процесса используется функция VirtualQueryEx и специальная структура MEMORY_BASIC_INFORMATION. В FASM она как обычно не определена, придется сделать это самостоятельно в сегменте данных:

; Структура для чтения памяти процесса
struct  MEMORY_BASIC_INFORMATION
        BaseAddress          dd ?
        AllocationBase       dd ?
        AllocationProtect    dd ?
        RegionSize           dd ?
        State                dd ?
        Protect              dd ?
        Type                 dd ?
ends
 
; Структура для чтения памяти процесса
mbi             MEMORY_BASIC_INFORMATION

Для работы с памятью процесса при помощи функции VirtualQueryEx нам надо знать хэндл этого процесса. Для дочерних, которые мы запустили сами, все просто: это значение hProcess из структуры PROCESS_INFORMATION. Если процесс порожден не нами, то для получения его хэндла, зная ID процесса, надо воспользоваться функцией OpenProcess, обязательно с флагом PROCESS_QUERY_INFORMATION. Возможно, что перед этим потребуется повысить привилегии нашего процесса, активировав SeDebugPrivilege.

Теперь у нас есть все инструменты и условия для работы, можно приступать к самому действу. Переменные, используемые в примере: ptMemory - указатель на текущий блок памяти, hProcess - хэндл исследуемого процесса, структура mbi определена выше.
        ...
        ; Начало памяти процесса
        mov     [ptMemory],0
.scan_memory:
        ; Получить данные о памяти процесса
        invoke  VirtualQueryEx, [hProcess], [ptMemory], mbi,\
                sizeof.MEMORY_BASIC_INFORMATION
        ; Сканирование закончено?
        or      eax,eax
        jz      .stop_scan
 
        ...
 
        ; Здесь можно выполнять все необходимые действия с блоком памяти:
        ; проверить или изменить его атрибуты, записать дамп памяти на 
        ; диск, поискать нужные сигнатуры и данные...
 
        ...
 
        ; Прибавить к указателю размер прочитанного региона
        mov     eax,[mbi.RegionSize]
        add     [ptMemory],eax
        jmp     .scan_memory
.stop_scan:
        ...

Блоки памяти процесса идут подряд, без разрывов, информация о каждом прочитанном блоке записывается в структуру mbi. Наиболее полезные для нас данные - это базовый адрес блока памяти mbi.BaseAddress и его размер mbi.RegionSize, а остальные, например, тип блока или флаги Memory Protection, помогают более точно определить нужный участок памяти. Все возможные значения флагов подробно описаны в MSDN.

В приложении консольная программа с исходниками. После запуска она предлагает выбрать исполняемый файл, запускает его и показывает на экране его карту памяти с указанием адресов, размеров, типа и атрибутов каждого блока.

Добавлено: 10 Апреля 2018 20:41:24 Добавил: Андрей Ковальчук

Как получить название текущего трека из Winamp и AIMP

Winamp был и остается самым популярным мультимедийным плеером для Windows. Такая популярность не могла остаться незамеченной, поэтому появились программы, использующие информацию из него в своих целях. Например, плагины для интернет-мессенджеров устанавливают название воспроизводимого трека в качестве статуса, а моя программа My Music Web Agent отправляет эту информацию в интернет. Для взаимодействия сторонних приложений с Winamp был разработан интерфейс API со своими командами и синтаксисом. По какой-то причине разработчики Winamp сейчас вообще убрали с сайта информацию об API, но в интернете эти данные остались. Я приложил описание Winamp Application Programming Interface в архиве с примером программы, рекомендую ознакомиться с ним перед прочтением статьи, чтобы не возникало вопросов откуда взялись те или иные значения.

; Сегмент данных
section '.data' data readable writeable
 
; Название класса окна Winamp/AIMP
wcl      db      'Winamp v1.x',0
 
buff     rb 300h  ; Буфер для получения текста из заголовка окна
play_now rb 300h  ; Буфер для получения названия трека

Плеер Winamp, как и другие приложения для Windows, принимает и обрабатывает сообщения, отправляемые ему через SendMessage. Но для этого сперва надо определить хэндл его окна. Сделать это очень просто, окно Winamp всегда имеет название класса "Winamp v1.x", поэтому воспользуемся функцией FindWindow. Дальше надо узнать состояние плеера. Для этого надо послать окну Winamp сообщение WM_USER с wParam = 0 и lParam = 104. Как написано в документации про это сообщение: "Returns the status of playback. If 'ret' is 1, Winamp is playing. If 'ret' is 3, Winamp is paused. Otherwise, playback is stopped." То есть, если вернулось значение не равное 1, то Winamp ничего не воспроизводит или находится в режиме паузы.
; Сегмент кода
section '.code' code readable executable
        ...
        ; Получить хэндл окна Winamp
        invoke  FindWindow,wcl,0
        or      eax,eax
        ; Окно Winamp не найдено
        jz      .no_winamp
 
        ; Сохранить хэндл окна
        mov     ebx,eax
 
        ; Что-то сейчас воспроизводится?
        invoke  SendMessage,ebx,WM_USER,NULL,104
        cmp     eax,1
        ; Winamp не находится в состоянии "Play"
        jne     .no_winamp
        ...

Окно мы нашли, состояние проигрывателя знаем, осталось получить название трека, который сейчас воспроизводится. Казалось бы все просто - надо послать еще одно сообщение, которое также описано в документации, и получить результат. Но не тут-то было. Сообщения для получения информации о воспроизводимом файле и названии трека доступны только из контекста процесса самого Winamp, то есть могут использоваться только в плагинах Winamp. Логика разработчиков тут не совсем ясна, ведь для сторонних приложений доступна информация о битрейте текущего трека, размере плей-листа, текущей позиции воспроизведения и еще куча другой малополезной информации, а простейшего названия трека нет.

Городить огороды с плагинами мы не будем, а повнимательнее присмотримся к найденному окну, то которое "Winamp 1.x". Оказывается, Winamp при воспроизведении устанавливает в его заголовок номер и название текущего трека. А именно это нам и надо! Функция GetWindowText решает все наши проблемы.
        ...
        ; Получить текст из заголовка окна
        invoke  GetWindowText,ebx,buff,300h
        or      eax,eax
        ; Непонятная ошибка - текст пустой
        jz      .no_winamp
        ...

Строка с названием трека есть, но она имеет вид наподобие "2. Metallica - Master of Puppets - Winamp". В принципе, можно оставить и так, но мы пойдем до победного финала. Для пущей эстетики приведем строку к нормальному виду, то есть отрежем номер трека в начале и суффикс " - Winamp" в конце.
        ...
        ; Удалить из строки ' - Winamp'
        mov     esi,buff
        cmp     dword [esi+eax-9],' - W'
        jne     @f
        mov     byte [esi+eax-9],0
 
        ; Удалить из строки номер трека
@@:
        lodsb
        cmp     al,' '
        jne     @b
 
        ; Скопировать название трека
        invoke  lstrcpy,play_now,esi
 
        ; Теперь в буфере play_now содержится название трека
        ...

Примечательно, что некоторые другие мультимедийные плееры, например, AIMP и Apollo также создают окно с именем класса "Winamp 1.x", и, как вы наверное догадались, с полностью аналогичными свойствами. Так что описанный в этой статье метод получения названия воспроизводимого трека подходит и для них.

В приложении пример программы с исходным текстом, получающей в режиме реального времени название воспроизводимого трека из плеера Winamp и совместимых с ним, а также официальная документация Winamp Application Programming Interface.

Добавлено: 10 Апреля 2018 20:40:48 Добавил: Андрей Ковальчук

Перехват ввода и вывода консольных программ

Перехват ввода и вывода консольных программ бывает нужен, когда требуется получить результат их работы для обработки в нашем приложении. Также мы получаем возможность передавать консольным программам собственные данные. Как обычно в FASM'е готовых решений нет, пришлось разбираться самому и портировать с языков высокого уровня. Технически перехват ввода и вывода консоли выполняется с использованием специальных структур, называемых "Pipe". По принципу действия они и вправду похожи на трубы: в один конец информация "вливается", из другого "выливается", а перехват является просто подключением нашего "крана" к тому или иному концу трубы. Для перехвата требуется переопределить стандартные дескрипторы ввода и вывода консольного приложения на наши. Создать новые дескрипторы можно при помощи функции CreatePipe, а затем прописать в структуру STARTUPINFO запускаемого приложения. После этого новые дескрипторы будут доступны для чтения и записи как обычный файл.

В сегменте данных родительского приложения требуется определить следующие переменные и структуры:

; Сегмент данных
section '.data' data readable writeable 
 
; Данные для перехвата консоли
newstdin      dd ?  ; Новый дескриптор стандартного ввода
newstdout     dd ?  ; Новый дескриптор стандартного вывода
read_stdout   dd ?  ; Дескриптор для использования ReadFile
write_stdin   dd ?  ; Дескриптор для использования WriteFile
bytestoread   dd ?  ; Всего байт в буфере консоли
available     dd ?  ; Счетчик байт, доступных для чтения из консоли
 
; Эта структура по умолчанию не определена, сделаем это сами
struct SECURITY_ATTRIBUTES
       nLength               dd ?
       lpSecurityDescriptor  dd ?
       bInheritHandle        dd ?
ends
 
; Описание структур для запуска консольной программы и настройки дескрипторов
sinfo         STARTUPINFO
sattr         SECURITY_ATTRIBUTES
pinfo         PROCESS_INFORMATION
 
; Дополнительно зарезервируем буфер для чтения информации
buff   rb 1024

Буфер большого размера для чтения данных лучше не использовать, вполне достаточно 1 килобайта. Количество байт, доступных для чтения из консоли, можно получить при помощи функции PeekNamedPipe. Обратите внимание, что фактически данные из консоли при этом не забираются, это надо будет сделать при помощи функции чтения файла ReadFile. Вот пример кода перехватчика вывода консоли.
; Сегмент кода
section '.code' code readable executable
        ; Заполнить структуру SECURITY_ATTRIBUTES
        mov     [sattr.nLength],sizeof.SECURITY_ATTRIBUTES
        mov     [sattr.lpSecurityDescriptor],NULL
        mov     [sattr.bInheritHandle],TRUE
 
        ; Создать новые дескрипторы для консольного ввода и вывода
        invoke  CreatePipe,newstdin,write_stdin,sattr,NULL
        invoke  CreatePipe,read_stdout,newstdout,sattr,NULL
 
        ; Заполнить структуру STARTUPINFO данными процесса
        invoke  GetStartupInfo,sinfo
        ; Установить флаги: использовать собственные дескрипторы
        ; ввода-вывода и возможность изменять видимость окна процесса
        mov     [sinfo.dwFlags],STARTF_USESTDHANDLES+STARTF_USESHOWWINDOW
        mov     [sinfo.wShowWindow],SW_HIDE
        mov     eax,[newstdout]
        ; Установить наш дескриптор для стандартного вывода и ошибок
        mov     [sinfo.hStdOutput],eax
        mov     [sinfo.hStdError],eax
        mov     eax,[newstdin]
        ; Установить наш дескриптор для стандартного ввода
        mov     [sinfo.hStdInput],eax
 
        ; Запустить консольное приложение в режиме suspended
        invoke  CreateProcess, NULL, fname, NULL, NULL, TRUE,\
                CREATE_SUSPENDED+NORMAL_PRIORITY_CLASS+CREATE_NEW_CONSOLE,\
                NULL,NULL,sinfo,pinfo
 
        ; Тут можно выполнить какие-то промежуточные действия, например
        ; спросить у пользователя куда сохранять файл, или проверить
        ; доступность каких-либо данных
 
        ; Отпустить замороженный процесс на выполнение
        invoke  ResumeThread,[pinfo.hThread]
wait_for_finish:
        ; Проверить активен ли еще запущенный процесс
        invoke  GetExitCodeProcess,[pinfo.hProcess],tmp
        cmp     [tmp],STILL_ACTIVE
        jne     finish
 
        ; Небольшая пауза чтобы не грузить систему
        invoke  Sleep,10
 
        ; Прочитать данные из буфера консоли без удаления
        invoke  PeekNamedPipe,[read_stdout],buff,1023,bytestoread,available,NULL
        ; Если консоль еще ничего не вывела, то продолжать ждать результат
        cmp     [bytestoread],0
        je      wait_for_finish
read_data_from_pipe:
        ; Прочитать данные из буфера консоли и записать в наш файл
        invoke  ReadFile,[read_stdout],buff,1023,bytestoread,NULL
 
        ; Теперь в буфере buff содержатся данные из консоли. Их можно
        ; проанализировать, вывести в форму оконного приложения, сохранить
        ; в файл или просто проигнорировать
 
        ; Дополнительная проверка если весь вывод был меньше размера буфера
        cmp     [available],1023
        jna     wait_for_finish
 
        ; Продолжать чтение до окончания данных
        cmp     [bytestoread],1023
        je      read_data_from_pipe
        jmp     wait_for_finish
finish:
        ; Завершить обработку

Если не требуется обработка данных из консольного приложения, то можно сразу перенаправить его, например, на дескриптор открытого файла или принтер. Для передачи данных в поток консольного приложения надо отправлять их через функцию WriteFile на дескриптор, описанный в этом примере как write_stdin.

В аттаче рабочий пример программы, перехватывающей консольный вывод и сохраняющей его в файл console.txt. В примере выполняется команда "cmd /c dir c:\", выводящая список файлов и каталогов в корне диска C:

Добавлено: 10 Апреля 2018 20:39:52 Добавил: Андрей Ковальчук

Управление клавишами NumLock, CapsLock и ScrollLock

Иногда в программах требуется получать состояние управляющих клавиш или изменять их состояние. Во времена MS-DOS достаточно было просто прочитать или записать значение WORD по определенному адресу памяти, при этом светодиодные индикаторы клавиатуры реагировали на это включением или выключением. Были очень популярны крохотные, в несколько байт, программы для выключения NumLock при загрузке системы, типа таких:

;-----------------------------------------------------------
; Программа для выключения индикатора NumLock под MS-DOS
; Размер .com-файла после компиляции 9 байт
;-----------------------------------------------------------
.286
.model  tiny
.code                         ; Сегмент кода
        org     100h          ; Зарезервировано для PSP
start:
        pop     ax            ; После запуска в стеке 0, AX=0
        mov     ds,ax         ; DS=0
        mov     ds:[417h],ax  ; WORD DS:[417h] - состояние *Lock'ов
        int     20h           ; Выход из программы
end     start

В свое время это был самый минимальный рабочий код для выключения клавиши NumLock, хоть и не совсем корректный в плане работы со стеком.

В среде Windows все значительно усложнилось. Теперь получать состояние переключателей приходится при помощи функции GetKeyState. Для изменения состояния клавиш в Windows есть функция SetKeyboardState, но она не меняет состояния светодиодов на клавиатуре. Поэтому придется искать другой путь, например, через функцию keybd_event эмулировать нажатия на соответствующие клавиши. Для удобства я написал небольшую функцию, устанавливающую нужный переключатель в нужное состояние.
;------------------------------------------------------------------
; Функция переключения индикаторов
; Входные параметры:
;    dKey   - идентификатор клавиши
;    dState - состояние (TRUE = включено, FALSE = выключено)
;------------------------------------------------------------------
proc SetLockState dKey:DWORD, dState:DWORD
        pusha
 
        ; Получить состояние управляющей клавиши
        invoke  GetKeyState,[dKey]
        ; Если клавиша уже в нужном положении, то ничего не делать
        cmp     eax,[dState]
        je      @f
 
        ; Имитировать нажатие клавиши на клавиатуре
        invoke  keybd_event,[dKey],0,0,NULL
        invoke  keybd_event,[dKey],0,KEYEVENTF_KEYUP,NULL
@@:
        popa
        ret
endp

Параметры вызова: dKey - код клавиши, для этого в FASM уже предопределены константы VK_CAPITAL, VK_NUMLOCK и VK_SCROLL, dState - нужное состояние (TRUE - включено, FALSE - выключено). Пример вызова:
        ...
        stdcall SetLockState,VK_CAPITAL,FALSE  ; Выключить CapsLock
        stdcall SetLockState,VK_NUMLOCK,TRUE   ; Включить NumLock
        ...

В приложении программа, управляющая индикаторами и состоянием клавиш NumLock, CapsLock и ScrollLock. Обратной связи не предусмотрено, поэтому ручные нажатия на клавиши не меняют состояния checkbox'ов.

Добавлено: 10 Апреля 2018 20:39:43 Добавил: Андрей Ковальчук

Программное выключение монитора

Выключить монитор из программы можно, но только если он совместим со стандартом EnergyStar. Об этом можно не беспокоиться, все современные мониторы удовлетворяют этому условию. Беспокоиться надо о другом. По непонятной причине с сайта на сайт упорно копируют один и тот же код на разных языках программирования, который якобы должен выключить монитор. Вот его вариант на Ассемблере:

; ВНИМАНИЕ! Это НЕПРАВИЛЬНЫЙ код!!!
invoke  GetDesktopWindow
invoke  SendMessage, eax, WM_SYSCOMMAND, SC_MONITORPOWER, 0

Во-первых, через хэндл из функции GetDesktopWindow достучаться до монитора не получится, причем ни в обычном Explorer'e, ни в альтернативных шеллах типа Aston Desktop. Чтобы сообщение дошло до нужного адресата, надо использовать широковещательную рассылку через HWND_BROADCAST. Во-вторых, непонятно откуда взялся последний параметр - 0. В MSDN четко прописано, что для выключения монитора через SC_MONITORPOWER значение lParam должно быть равно 2. Более того, нулевого значения параметра для этого сообщения вообще не предусмотрено. В двух строчках кода две принципиальные ошибки! И это уже далеко не первый случай, когда код из различных популярных источников является заведомо нерабочим. Всем любителям бездумного копипаста очень рекомендую сперва сверяться с первоисточниками, а перед публикацией проверять весь код на практике.

Но хватит о грустном. Правильный код программного выключения монитора будет таким:
;-------------------------------------------------
; Правильный код выключения монитора
;-------------------------------------------------
; В FASM не определена константа HWND_BROADCAST, сделаем это самостоятельно
HWND_BROADCAST = 0FFFFh
; Выключить монитор
invoke  SendMessage, HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2

Для включения монитора обратно достаточно пошевелить мышкой или нажать любую кнопку на клавиатуре. А можно также сделать это из нашей программы. У копипастеров этот код также содержит ошибку, потому что для включения монитора значение lParam должно быть равно -1, а не 1.
;-------------------------------------------------
; Правильный код включения монитора
;-------------------------------------------------
; В FASM не определена константа HWND_BROADCAST, сделаем это самостоятельно
HWND_BROADCAST = 0FFFFh
; Включить монитор
invoke  SendMessage, HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, -1

В приложении пример программы, которая через секунду после запуска выключит монитор, а еще через 5 секунд включит его обратно.

Добавлено: 10 Апреля 2018 20:38:40 Добавил: Андрей Ковальчук

Задача на применение логических инструкций

Когда-то кому-то помогал с решением задачи на ассемблере, формулировка задания была такая:

Дан массив из 5 байт. Рассматривая его как массив из восьми 5-битных слов, посчитать количество слов с четным числом единиц в слове.

Может быть готовое решение пригодится кому-нибудь еще. Комментарии не прописывал, код и так достаточно простой.
;---------------------------------------------------------------
; Задача на применение логических инструкций
;
; Дан массив из 5 байт. Рассматривая его как массив из восьми
; 5-битных слов, посчитать количество слов с четным числом
; единиц в слове.
;
; Решение: ManHunter / PCL
;---------------------------------------------------------------
 
format PE GUI 4.0
entry start
 
include 'win32a.inc'
 
;---------------------------------------------------------------
 
section '.data' data readable writeable
 
xbytes   db 00111000b ; Данные для задачки, взяты с потолка :)
         db 11111110b
         db 01010101b
         db 00001001b
         db 00000110b
 
mask     db 'Count: %i',13,10,13,10
         db '%i%i%i%i%i - %i%i%i%i%i - %i%i%i%i%i - %i%i%i%i%i',13,10
         db '%i%i%i%i%i - %i%i%i%i%i - %i%i%i%i%i - %i%i%i%i%i',13,10
        db 0
 
title    db 'Solution',0
tmp      rb 100
 
;---------------------------------------------------------------
 
section '.code' code readable executable
start:
         mov     esi,xbytes+4
loc_1:
         lodsb
         mov     ecx,8
loc_2:
         xor     edx,edx
         test    al,00000001b
         jz      loc_3
         inc     edx
loc_3:
         push    edx
         shr     al,1
         loop    loc_2
 
         dec     esi
         dec     esi
         cmp     esi,xbytes
         jnb     loc_1
 
         xor     eax,eax
         xor     esi,esi
loc_4:
         xor     edi,edi
         mov     ecx,5
loc_5:
         add     edi,[esp+eax*4]
         inc     eax
         loop    loc_5
 
         test    edi,edi
         jz      loc_6
         test    edi,1
         jnz     loc_6
         inc     esi
loc_6:
         cmp     eax,40
         jb      loc_4
 
         invoke  wsprintf,tmp,mask,esi
         add     esp,12+(8*5*4)
 
         invoke  MessageBox,HWND_DESKTOP,tmp,title,MB_OK
         invoke  ExitProcess,0
 
;---------------------------------------------------------------
 
section '.idata' import data readable writeable
 
library kernel32,"KERNEL32.DLL",\
         user32,"USER32.DLL"
 
include "apia\kernel32.inc"
include "apia\user32.inc"

Исходник и скомпилированный файл прилагаются.

Добавлено: 10 Апреля 2018 20:37:55 Добавил: Андрей Ковальчук