Git за полчаса: руководство для начинающих

В последние годы популярность git демонстрирует взрывной рост. Эта система контроля версий используется различными проектами с открытым исходным кодом.

Новичков часто пугает большое количество замысловатых команд и сложных аргументов. Но для начала все они и не нужны. Можно начать с изучения наиболее часто используемых команд, и после этого постепенно расширять свои знания. Именно так мы и поступим в этой статье. Поехали!

Основы
Git — это набор консольных утилит, которые отслеживают и фиксируют изменения в файлах (чаще всего речь идет об исходном коде программ, но вы можете использовать его для любых файлов на ваш вкус). С его помощью вы можете откатиться на более старую версию вашего проекта, сравнивать, анализировать, сливать изменения и многое другое. Этот процесс называется контролем версий. Существуют различные системы для контроля версий. Вы, возможно, о них слышали: SVN, Mercurial, Perforce, CVS, Bitkeeper и другие.

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

Установка
Установить git на свою машину очень просто:

Linux — нужно просто открыть терминал и установить приложение при помощи пакетного менеджера вашего дистрибутива. Для Ubuntu команда будет выглядеть следующим образом:

sudo apt-get install git

Windows — мы рекомендуем git for windows, так как он содержит и клиент с графическим интерфейсом, и эмулятор bash.
OS X — проще всего воспользоваться homebrew. После его установки запустите в терминале:
brew install git

Если вы новичок, клиент с графическим интерфейсом(например GitHub Desktop и Sourcetree) будет полезен, но, тем не менее, знать команды очень важно.

Настройка
Итак, мы установили git, теперь нужно добавить немного настроек. Есть довольно много опций, с которыми можно играть, но мы настроим самые важные: наше имя пользователя и адрес электронной почты. Откройте терминал и запустите команды:

git config --global user.name "My Name"
git config --global user.email myEmail@example.com

Теперь каждое наше действие будет отмечено именем и почтой. Таким образом, пользователи всегда будут в курсе, кто отвечает за какие изменения — это вносит порядок.

Создание нового репозитория
Как мы отметили ранее, git хранит свои файлы и историю прямо в папке проекта. Чтобы создать новый репозиторий, нам нужно открыть терминал, зайти в папку нашего проекта и выполнить команду init. Это включит приложение в этой конкретной папке и создаст скрытую директорию .git, где будет храниться история репозитория и настройки.
Создайте на рабочем столе папку под названием git_exercise. Для этого в окне терминала введите:

$ mkdir Desktop/git_exercise/
$ cd Desktop/git_exercise/
$ git init

Командная строка должна вернуть что-то вроде:

Initialized empty Git repository in /home/user/Desktop/git_exercise/.git/

Это значит, что наш репозиторий был успешно создан, но пока что пуст. Теперь создайте текстовый файл под названием hello.txt и сохраните его в директории git_exercise.

Определение состояния
status — это еще одна важнейшая команда, которая показывает информацию о текущем состоянии репозитория: актуальна ли информация на нём, нет ли чего-то нового, что поменялось, и так далее. Запуск git status на нашем свежесозданном репозитории должен выдать:

$ git status
On branch master
Initial commit
Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt

Сообщение говорит о том, что файл hello.txt неотслеживаемый. Это значит, что файл новый и система еще не знает, нужно ли следить за изменениями в файле или его можно просто игнорировать. Для того, чтобы начать отслеживать новый файл, нужно его специальным образом объявить.

Подготовка файлов
В git есть концепция области подготовленных файлов. Можно представить ее как холст, на который наносят изменения, которые нужны в коммите. Сперва он пустой, но затем мы добавляем на него файлы (или части файлов, или даже одиночные строчки) командой add и, наконец, коммитим все нужное в репозиторий (создаем слепок нужного нам состояния) командой commit.
В нашем случае у нас только один файл, так что добавим его:

$ git add hello.txt

Если нам нужно добавить все, что находится в директории, мы можем использовать

$ git add -A

Проверим статус снова, на этот раз мы должны получить другой ответ:

$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached ..." to unstage)
new file: hello.txt

Файл готов к коммиту. Сообщение о состоянии также говорит нам о том, какие изменения относительно файла были проведены в области подготовки — в данном случае это новый файл, но файлы могут быть модифицированы или удалены.

Коммит(фиксация изменений)
Коммит представляет собой состояние репозитория в определенный момент времени. Это похоже на снапшот, к которому мы можем вернуться и увидеть состояние объектов на определенный момент времени.
Чтобы зафиксировать изменения, нам нужно хотя бы одно изменение в области подготовки (мы только что создали его при помощи git add), после которого мы может коммитить:

$ git commit -m "Initial commit."

Эта команда создаст новый коммит со всеми изменениями из области подготовки (добавление файла hello.txt). Ключ -m и сообщение «Initial commit.» — это созданное пользователем описание всех изменений, включенных в коммит. Считается хорошей практикой делать коммиты часто и всегда писать содержательные комментарии.

Удаленные репозитории
Сейчас наш коммит является локальным — существует только в директории .git на нашей файловой системе. Несмотря на то, что сам по себе локальный репозиторий полезен, в большинстве случаев мы хотим поделиться нашей работой или доставить код на сервер, где он будет выполняться.

1. Подключение к удаленному репозиторию
Чтобы загрузить что-нибудь в удаленный репозиторий, сначала нужно к нему подключиться. В нашем руководстве мы будем использовать адрес https://github.com/tutorialzine/awesome-project, но вам посоветуем попробовать создать свой репозиторий в GitHub, BitBucket или любом другом сервисе. Регистрация и установка может занять время, но все подобные сервисы предоставляют хорошую документацию.
Чтобы связать наш локальный репозиторий с репозиторием на GitHub, выполним следующую команду в терминале. Обратите внимание, что нужно обязательно изменить URI репозитория на свой.

# This is only an example. Replace the URI with your own repository address.
$ git remote add origin https://github.com/tutorialzine/awesome-project.git

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

2. Отправка изменений на сервер
Сейчас самое время переслать наш локальный коммит на сервер. Этот процесс происходит каждый раз, когда мы хотим обновить данные в удаленном репозитории.
Команда, предназначенная для этого - push. Она принимает два параметра: имя удаленного репозитория (мы назвали наш origin) и ветку, в которую необходимо внести изменения (master — это ветка по умолчанию для всех репозиториев).

$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 212 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/tutorialzine/awesome-project.git
* [new branch] master -> master

В зависимости от сервиса, который вы используете, вам может потребоваться аутентифицироваться, чтобы изменения отправились. Если все сделано правильно, то когда вы посмотрите в удаленный репозиторий при помощи браузера, вы увидите файл hello.txt

3. Клонирование репозитория
Сейчас другие пользователи GitHub могут просматривать ваш репозиторий. Они могут скачать из него данные и получить полностью работоспособную копию вашего проекта при помощи команды clone.

$ git clone https://github.com/tutorialzine/awesome-project.git

Новый локальный репозиторий создается автоматически с GitHub в качестве удаленного репозитория.

4. Запрос изменений с сервера
Если вы сделали изменения в вашем удаленном репозитории, другие пользователи могут скачать изменения при помощи команды pull.

$ git pull origin master
From https://github.com/tutorialzine/awesome-project
* branch master -> FETCH_HEAD
Already up-to-date.

Так как новых коммитов с тех пор, как мы склонировали себе проект, не было, никаких изменений доступных для скачивания нет.

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

Уже рабочая, стабильная версия кода сохраняется.
Различные новые функции могут разрабатываться параллельно разными программистами.
Разработчики могут работать с собственными ветками без риска, что кодовая база поменяется из-за чужих изменений.
В случае сомнений, различные реализации одной и той же идеи могут быть разработаны в разных ветках и затем сравниваться.
1. Создание новой ветки
Основная ветка в каждом репозитории называется master. Чтобы создать еще одну ветку, используем команду branch <name>

$ git branch amazing_new_feature

Это создаст новую ветку, пока что точную копию ветки master.

2. Переключение между ветками
Сейчас, если мы запустим branch, мы увидим две доступные опции:

$ git branch
amazing_new_feature
* master

master — это активная ветка, она помечена звездочкой. Но мы хотим работать с нашей “новой потрясающей фичей”, так что нам понадобится переключиться на другую ветку. Для этого воспользуемся командой checkout, она принимает один параметр — имя ветки, на которую необходимо переключиться.

$ git checkout amazing_new_feature

3. Слияние веток
Наша “потрясающая новая фича” будет еще одним текстовым файлом под названием feature.txt. Мы создадим его, добавим и закоммитим:

$ git add feature.txt
$ git commit -m "New feature complete.”

Изменения завершены, теперь мы можем переключиться обратно на ветку master.

$ git checkout master

Теперь, если мы откроем наш проект в файловом менеджере, мы не увидим файла feature.txt, потому что мы переключились обратно на ветку master, в которой такого файла не существует. Чтобы он появился, нужно воспользоваться merge для объединения веток (применения изменений из ветки amazing_new_feature к основной версии проекта).

$ git merge amazing_new_feature

Теперь ветка master актуальна. Ветка amazing_new_feature больше не нужна, и ее можно удалить.

$ git branch -d awesome_new_feature

Дополнительно
В последней части этого руководства мы расскажем о некоторых дополнительных трюках, которые могут вам помочь.

1. Отслеживание изменений, сделанных в коммитах
У каждого коммита есть свой уникальный идентификатор в виде строки цифр и букв. Чтобы просмотреть список всех коммитов и их идентификаторов, можно использовать команду log:
[spoiler title='Вывод git log']

$ git log
commit ba25c0ff30e1b2f0259157b42b9f8f5d174d80d7
Author: Tutorialzine
Date: Mon May 30 17:15:28 2016 +0300
New feature complete
commit b10cc1238e355c02a044ef9f9860811ff605c9b4
Author: Tutorialzine
Date: Mon May 30 16:30:04 2016 +0300
Added content to hello.txt
commit 09bd8cc171d7084e78e4d118a2346b7487dca059
Author: Tutorialzine
Date: Sat May 28 17:52:14 2016 +0300
Initial commit
[/PLAIN]
Как вы можете заметить, идентификаторы довольно длинные, но для работы с ними не обязательно копировать их целиком — первых нескольких символов будет вполне достаточно. Чтобы посмотреть, что нового появилось в коммите, мы можем воспользоваться командой show
[PLAIN][spoiler title='Вывод git show']

$ git show b10cc123
commit b10cc1238e355c02a044ef9f9860811ff605c9b4
Author: Tutorialzine
Date: Mon May 30 16:30:04 2016 +0300
Added content to hello.txt
diff --git a/hello.txt b/hello.txt
index e69de29..b546a21 100644
--- a/hello.txt
+++ b/hello.txt
@@ -0,0 +1 @@
+Nice weather today, isn't it?
[/PLAIN]
Чтобы увидеть разницу между двумя коммитами, используется команда diff (с указанием промежутка между коммитами):
[spoiler title='Вывод git diff']

$ git diff 09bd8cc..ba25c0ff
diff --git a/feature.txt b/feature.txt
new file mode 100644
index 0000000..e69de29
diff --git a/hello.txt b/hello.txt
index e69de29..b546a21 100644
--- a/hello.txt
+++ b/hello.txt
@@ -0,0 +1 @@
+Nice weather today, isn't it?
[/PLAIN]
Мы сравнили первый коммит с последним, чтобы увидеть все изменения, которые были когда-либо сделаны. Обычно проще использовать git difftool, так как эта команда запускает графический клиент, в котором наглядно сопоставляет все изменения.

2. Возвращение файла к предыдущему состоянию
Гит позволяет вернуть выбранный файл к состоянию на момент определенного коммита. Это делается уже знакомой нам командой checkout, которую мы ранее использовали для переключения между ветками. Но она также может быть использована для переключения между коммитами (это довольно распространенная ситуация для Гита - использование одной команды для различных, на первый взгляд, слабо связанных задач).
В следующем примере мы возьмем файл hello.txt и откатим все изменения, совершенные над ним к первому коммиту. Чтобы сделать это, мы подставим в команду идентификатор нужного коммита, а также путь до файла:

$ git checkout 09bd8cc1 hello.txt

3. Исправление коммита
Если вы опечатались в комментарии или забыли добавить файл и заметили это сразу после того, как закоммитили изменения, вы легко можете это поправить при помощи commit —amend. Эта команда добавит все из последнего коммита в область подготовленных файлов и попытается сделать новый коммит. Это дает вам возможность поправить комментарий или добавить недостающие файлы в область подготовленных файлов.
Для более сложных исправлений, например, не в последнем коммите или если вы успели отправить изменения на сервер, нужно использовать revert. Эта команда создаст коммит, отменяющий изменения, совершенные в коммите с заданным идентификатором.
Самый последний коммит может быть доступен по алиасу HEAD:

$ git revert HEAD

Для остальных будем использовать идентификаторы:

$ git revert b10cc123

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

4. Разрешение конфликтов при слиянии
Помимо сценария, описанного в предыдущем пункте, конфликты регулярно возникают при слиянии ветвей или при отправке чужого кода. Иногда конфликты исправляются автоматически, но обычно с этим приходится разбираться вручную — решать, какой код остается, а какой нужно удалить.
Давайте посмотрим на примеры, где мы попытаемся слить две ветки под названием john_branch и tim_branch. И Тим, и Джон правят один и тот же файл: функцию, которая отображает элементы массива.
Джон использует цикл:

// Use a for loop to console.log contents.
for(var i=0; i<arr.length; i++) {
console.log(arr[i]);
}
Тим предпочитает forEach:

// Use forEach to console.log contents.
arr.forEach(function(item) {
console.log(item);
});

Они оба коммитят свой код в соответствующую ветку. Теперь, если они попытаются слить две ветки, они получат сообщение об ошибке:

$ git merge tim_branch
Auto-merging print_array.js
CONFLICT (content): Merge conflict in print_array.js
Automatic merge failed; fix conflicts and then commit the result.

Система не смогла разрешить конфликт автоматически, значит, это придется сделать разработчикам. Приложение отметило строки, содержащие конфликт:
[spoiler title='Вывод']

<<<<<<< HEAD // Use a for loop to console.log contents. for(var i=0; i<arr.length; i++) { console.log(arr[i]); } ======= // Use forEach to console.log contents. arr.forEach(function(item) { console.log(item); }); >>>>>>> Tim's commit.
[/PLAIN]
Над разделителем ======= мы видим последний (HEAD) коммит, а под ним - конфликтующий. Таким образом, мы можем увидеть, чем они отличаются и решать, какая версия лучше. Или вовсе написать новую. В этой ситуации мы так и поступим, перепишем все, удалив разделители, и дадим git понять, что закончили.

// Not using for loop or forEach.
// Use Array.toString() to console.log contents.
console.log(arr.toString());
Когда все готово, нужно закоммитить изменения, чтобы закончить процесс:

$ git add -A
$ git commit -m "Array printing conflict resolved."

Как вы можете заметить, процесс довольно утомительный и может быть очень сложным в больших проектах. Многие разработчики предпочитают использовать для разрешения конфликтов клиенты с графическим интерфейсом. (Для запуска нужно набрать git mergetool).

5. Настройка .gitignore
В большинстве проектов есть файлы или целые директории, в которые мы не хотим (и, скорее всего, не захотим) коммитить. Мы можем удостовериться, что они случайно не попадут в git add -A при помощи файла .gitignore

Создайте вручную файл под названием .gitignore и сохраните его в директорию проекта.
Внутри файла перечислите названия файлов/папок, которые нужно игнорировать, каждый с новой строки.
Файл .gitignore должен быть добавлен, закоммичен и отправлен на сервер, как любой другой файл в проекте.
Вот хорошие примеры файлов, которые нужно игнорировать:

Логи
Артефакты систем сборки
Папки node_modules в проектах node.js
Папки, созданные IDE, например, Netbeans или IntelliJ
Разнообразные заметки разработчика.
Файл .gitignore, исключающий все перечисленное выше, будет выглядеть так:

*.log
build/
node_modules/
.idea/
my_notes.txt

Символ слэша в конце некоторых линий означает директорию (и тот факт, что мы рекурсивно игнорируем все ее содержимое). Звездочка, как обычно, означает шаблон.

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

Официальная документация, включающая книгу и видеоуроки – тут.
“Getting git right” – Коллекция руководств и статей от Atlassian – тут.
Список клиентов с графическим интерфейсом – тут.
Онлайн утилита для генерации .gitignore файлов – тут.
Оригинал статьи доступен на сайте http://tutorialzine.com

Добавлено: 22 Апреля 2021 07:16:54 Добавил: Андрей Ковальчук

Мой подход к работе с Git

Второй день знакомлюсь с Git. Читаю книжку Pro Git, попутно загоняя буковки в консоль =)

Расскажу, как организовал процесс разработки на своём компьютере. Если что-то не правильно или есть лучшие способы, то смело пишите в комментах!

Более опытные коллеги подсказали, что ставить на локальный компьютер "Git сервер" не очень разумно, лучше обойтись одной папкой в которой будут размещаться голые (bare) репозитории и которая будет служить центральным хранилищем.

Итак. Создаём папку под голые репозитории, например C:\GitRepos (да да, я сижу на Windows):

$ mkdir /c/GitRepos

Создаём голый репозиторий myproject.git:

$ cd /c/GitRepos
$ mkdir myproject.git
$ cd myproject.git
$ git init --bare

Переходим в каталог, в котором располагаются исходники проекта myproject и создаём там новый локальный репозиторий:

$ git init

Связываем его с основным:

$ git remote add origin /c/GitRepos/myproject.git

Добавляем в локальный репозиторий файлы и делаем первый коммит:

$ git add .
$ git commit -a -m 'First commit'

Отправляем проект на "сервер" (в папку C:\GitRepos):

$ git push origin master

Теперь чтобы продолжить разработку myproject в другом месте, нужно сделать копию основного репозитория:

$ git clone /c/GitRepos/myproject.git

и после очередного коммита в локальный репозиторий, обновить основной:

$ git push

Получить свежую версию из основного репозитория, можно так:

$ git pull

Добавлено: 27 Июля 2018 22:52:42 Добавил: Андрей Ковальчук

Полезные параметры командной строки

Версия PHP

# php -v
PHP 5.2.12 (cli) (built: Mar 01 2010 12:00:00)
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2009 Zend Technologies

Список установленных расширений
# php -m
[PHP Modules]
ctype
curl
date
iconv
...

Аналог phpinfo()
# php -i

Список прочитанных конфигов
# php --ini
Configuration File (php.ini) Path: /usr/local/etc
Loaded Configuration File:         /usr/local/etc/php.ini
Scan for additional .ini files in: /usr/local/etc/php
Additional .ini files parsed:      /usr/local/etc/php/extensions.ini

Добавлено: 22 Июля 2018 10:03:32 Добавил: Андрей Ковальчук

Выполнение нескольких команд в консоли Windows (cmd)

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

команда1 & команда2 — Используется для разделения нескольких команд в одной командной строке. В cmd.exe выполняется первая команда, затем вторая команда.

команда1 && команда2 — Запускает команду, стоящую за символом &&, только если команда, стоящая перед этим символом была выполнена успешно. В cmd.exe выполняется первая команда. Вторая команда выполняется, только если первая была выполнена успешно.

команда1 || команда2 — Запускает команду, стоящую за символом ||, только если команда, стоящая перед символом || не была выполнена. В cmd.exe выполняется первая команда. Вторая команда выполняется, только если первая не была выполнена (полученный код ошибки превышает ноль).

Пример:

attrib -H "file.txt" && ren "file.txt" "file.tmp"

С файла file.txt будет снят атрибут "Скрытый" и если команда attrib при этом не вернёт ошибку, файл будет переименован в file.tmp.

Добавлено: 18 Июля 2018 07:40:08 Добавил: Андрей Ковальчук

Диалог открытия файлов и юзабилити Windows

При всех удобствах Windows некоторые моменты меня очень сильно раздражают. Особенно поведение системы при вызове диалогов открытия файлов. Сперва немного предыстории. При работе с файлами через функцию GetOpenFileName или GetSaveFileName в структуре OPENFILENAME есть возможность указать путь, который должен открыться по умолчанию. Если это значение не задано, то система сама где-то запоминает папку, в которой последний раз был удачно открыт файл (то есть окно выбора файла было закрыто через кнопку "Ok"). Где именно хранится эта информация - я пока не выяснил, да и не особо надо. Второй вариант. Предположим, что некоторая программа самостоятельно запоминает путь к папке, в которой последний раз ею выполнялись какие-то действия с файлами. Это может быть, например, текстовый редактор, просмотрщик графики и т.п., не суть. Главное, что задумка очень хорошая и правильная. При следующем запуске или вызове диалога выбора файла в соответствующее поле OPENFILENAME будет подставлен сохраненный путь и пользователь продолжит работу с того места, где он в прошлый раз остановился. Что-то типа такого:

        ...
        invoke  GetModuleHandle,0
        mov     [ofn.hInstance],eax
        mov     [ofn.lStructSize], sizeof.OPENFILENAME
        mov     [ofn.hwndOwner],0
        mov     [ofn.nMaxFile],MAX_PATH
        mov     [ofn.lpstrFile],buff
        ; Открывать с последней сохраненной папки
        mov     [ofn.lpstrInitialDir],saved_dir
        mov     [ofn.Flags],OFN_EXPLORER+OFN_FILEMUSTEXIST
        invoke  GetOpenFileName,ofn
        ...

Неадекватное, на мой взгляд, поведение системы заключается в следующем. Вполне может возникнуть ситуация, что какая-то часть из сохраненного или запрошенного пути пропала. Например, я в просмотрщике рассортировал папку с фотографиями, в графическом редакторе подправил несколько файлов, а затем в файловом менеджере перенес всю папку с фотографиями в другое место на диске. В этом случае при попытке вернуться к просмотру в просмотрщике, повторно вызвать диалог открытия или сохранения файла в графическом редакторе, при любом раскладе в качестве дефолтного пути будет открыта какая-нибудь херня типа Библиотеки, Моих документов или вообще папки, куда установлена программа. Закономерности я тут тоже не уловил, видимо принятие решения остается за Windows и зависит от уровня осадков в Зимбабве. В итоге пользователю приходится снова топать весь путь из библиотеки до места работы.

После очередной серии чудесатых чудес я решил сделать для себя небольшую вспомогательную функцию. Она проверяет сохраненный путь и возвращает последнюю папку максимального уровня вложенности, которая существует на диске в текущий момент. Например, если ваша программа в последнем сеансе работы сохранила, а затем пытается открыть путь
D:\PICTURES\Путешествия\2011\Разобрать\Китай\NIKOND90\001

но при этом папки "\Китай" и, соответственно, вложенных в нее папок уже не существует, то должна открываться папка
D:\PICTURES\Путешествия\2011\Разобрать

и никак иначе! По-моему, это единственно правильное поведение системы. Почему разработчики Windows до сих пор открывают непонятно что вместо ПОСЛЕДНЕЙ ДОСТУПНОЙ папки из запрошенного пути - непонятно. Какая-то дефолтная папка может открываться только в одном единственном случае - когда ВЕСЬ запрошенный путь, включая букву диска, недоступен.
;------------------------------------------------------------
; Функция проверки доступности пути в файловой системе
; (C) ManHunter / PCL
; http://www.manhunter.ru
;------------------------------------------------------------
; Параметры:
; lpRaw - указатель на буфер размером MAX_PATH, в который
; записан проверяемый путь
; lpGood - указатель на буфер размером MAX_PATH, в который
; будет записан максимально доступный путь
;
; На выходе:
; EAX=0 - ни один из составляющих пути, включая носитель, не
; доступен
; EAX=1 - по крайней мере один из составляющих пути доступен,
; результат без финального слеша записан в буфер lpGood
;------------------------------------------------------------
proc    GetLastValidFolder lpRaw:DWORD, lpGood:DWORD
        locals
                result  dd ?
                old_dir rb MAX_PATH
                new_dir rb MAX_PATH
        endl
 
        pusha
 
        ; Сохранить текущую директорию
        lea     eax,[old_dir]
        invoke  GetCurrentDirectory,MAX_PATH,eax
 
        ; Скопировать поверяемый путь
        lea     esi,[new_dir]
        invoke  lstrcpy,esi,[lpRaw]
        mov     edi,esi
        invoke  lstrlen,esi
        or      eax,eax
        jz      .loc_bad
        dec     eax
        add     edi,eax
 
        ; Исправить слеши
.loc_fix_slash:
        cmp     byte [esi+eax],'/'
        jne     @f
        mov     byte [esi+eax],'\'
@@:
        dec     eax
        or      eax,eax
        jnz     .loc_fix_slash
 
.loc_chk:
        ; Попробовать установить текущую директорию
        invoke  SetCurrentDirectory,esi
        or      eax,eax
        jne     .loc_ok
.loc_scan:
        mov     byte [edi],0
        dec     edi
 
        ; Сканируем с конца до ближайшего слеша
        cmp     byte [edi],'\'
        je      .loc_chk
 
        ; Добрались до начала строки?
        cmp     edi,esi
        jne     .loc_scan
.loc_bad:
        ; Результат - ошибка
        mov     [result],0
        ; Обнулить строку
        mov     eax,[lpGood]
        mov     byte [eax],0
        jmp     .loc_ret
.loc_ok:
        ; Убрать финальный слеш
        cmp     byte [edi],'\'
        jne     @f
        mov     byte [edi],0
@@:
        ; Скопировать последний правильный путь
        invoke  lstrcpy,[lpGood],esi
        ; Результат - успешно
        mov     [result],1
.loc_ret:
        ; Вернуть на место текущую директорию
        lea     eax,[old_dir]
        invoke  SetCurrentDirectory,eax
 
        popa
        ; Записать результат в EAX
        mov     eax,[result]
        ret
endp

На входе передаются два указателя: lpRaw - указатель на исходную строку пути, lpGood - указатель на буфер-приемник, куда будет записан последний максимально доступный путь. Исходный путь может содержать не только папки, но и имя файла, в этом случае функция вернет только папки. Также функция исправляет слеши, приводя их к принятому в Windows виду "\". Результат выполнения возвращается в регистре EAX, если он равен 0, то не доступен ни один элемент проверяемого пути, включая диск. Если EAX=1, то доступный путь найден. Функция самодостаточная и не требует наличия каких-либо дополнительных переменных для своей работы.

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

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

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

Получение списка иконок в трее

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

Работать с вложенными окнами трея мы уже умеем, здесь практически то же самое, разница только в названиях классов дочерних окон.

; Сегмент данных
section '.data' data readable writeable 
...
class1  db 'Shell_TrayWnd',0    ; Название класса окна трея
class2  db 'TrayNotifyWnd',0    ; Название класса панели уведомлений
class3  db 'SysPager',0         ; Трей
class4  db 'ToolbarWindow32',0  ; Панель с иконками
 
ToolbarHandle   dd ?            ; Хэндл окна с иконками
...
; Сегмент кода
section '.code' code readable executable
        ...
        ; Найти окно трея
        invoke  FindWindow,class1,NULL
        or      eax,eax
        jz      exit_process
 
        ; Найти панель уведомлений
        invoke  FindWindowEx,eax,NULL,class2,NULL
        or      eax,eax
        jz      exit_process
 
        ; Найти трей
        invoke  FindWindowEx,eax,NULL,class3,NULL
        or      eax,eax
        jz      exit_process
 
        ; Найти панель иконок в трее
        invoke  FindWindowEx,eax,NULL,class4,NULL
        or      eax,eax
        jz      exit_process
 
        ; Сохранить хэндл окна с иконками
        mov     [ToolbarHandle],eax
        ...

Теперь у нас есть хэндл окна панели инструментов с иконками в трее. Получим количество иконок в панели.
        ; Получить количество иконок в трее
        invoke  SendMessage,eax,TB_BUTTONCOUNT,0,0
        or      eax,eax
        jz      exit_process
 
        ; Сохранить количество иконок в трее
        mov     [IconsCount],eax

Количество иконок тоже есть. Осталось перебрать их в цикле и получить всю необходимую информацию. Для этого используется сообщение TB_GETBUTTON и структура TBBUTTON для получения результата. Однако, если сейчас попробовать послать окну панели сообщение TB_GETBUTTON, то в результате не получим ничего. Почему? Потому что память, в которую будут записываться данные, обязательно должна принадлежать процессу, который является владельцем окна трея (обычно это explorer.exe).

Ненадолго отвлечемся от трея и выделим блок памяти нужного размера в контексте процесса-владельца трея. Размер блока равен размеру структуры TBBUTTON.
        ; Получить ID процесса-владельца трея
        invoke  GetWindowThreadProcessId,[ToolbarHandle],ProcId
        ; Открыть процесс с полным доступом
        invoke  OpenProcess,PROCESS_ALL_ACCESS,FALSE,[ProcId]
        or      eax,eax
        ; Фокус не удался
        jz      exit_process
 
        ; Сохранить хэндл процесса-владельца трея
        mov     [hProcess],eax
 
        ; Выделить блок памяти в контексте процесса
        invoke  VirtualAllocEx,[hProcess],NULL,dword sizeof.TBBUTTON,\
                MEM_COMMIT,PAGE_READWRITE
        or      eax,eax
        jz      exit_process
 
        ; Сохранить указатель на блок памяти
        mov     [lpData],eax

Теперь все готово для приема данных, можно приступать к перебору иконок в трее. В структуре TBBUTTON двойное слово dwData - указатель на блок расширенных данных, которые определяются приложением. В нашем случае по этому адресу лежит структура EXTRADATA, не описанная в FASM:
; Структура пользовательских данных иконки
struct EXTRADATA
        Wnd dd ?  ; Хэндл родительского окна иконки
        uID dd ?  ; Стиль отображения иконки
ends

Поскольку все нужные данные находятся в другом процессе, читать их будем через функцию ReadProcessMemory: сперва структуру TBBUTTON, а затем соответствующую ей структуру EXTRADATA. Зная хэндл окна-владельца каждой иконки, можно получить идентификатор процесса, которому принадлежит окно, а по нему, в свою очередь, можно узнать имя исполняемого файла. Для получения имени есть несколько методов, в этом примере я буду использовать функцию CreateToolhelp32Snapshot.
        ; Перебрать все иконки в трее
loc_loop:
        dec     [IconsCount]
 
        ; Получить иконку из трея с индексом IconsCount
        invoke  SendMessage,[ToolbarHandle],TB_GETBUTTON,[IconsCount],[lpData]
        ; Прочитать структуру иконки
        invoke  ReadProcessMemory,[hProcess],[lpData],button,\
                dword sizeof.TBBUTTON,BytesRead
        or      eax,eax
        jz      exit_process
        ; Прочиталась вся структура?
        cmp     [BytesRead],sizeof.TBBUTTON
        jnz     exit_process
 
        ; Прочитать пользовательские данные иконки
        invoke  ReadProcessMemory,[hProcess],[button.dwData],extra,\
                dword sizeof.EXTRADATA,BytesRead
        or      eax,eax
        jz      exit_process
        ; Прочиталась вся структура?
        cmp     [BytesRead],sizeof.EXTRADATA
        jnz     exit_process
 
        ; Это скрытая иконка?
        mov     eax,[extra.uID]
        and     eax,80000000h
        or      eax,eax
        ; Да, пропустить
        jnz     loc_loop
 
        ; Окно процесса существует?
        invoke  IsWindow,[extra.Wnd]
        or      eax,eax
        jz      loc_loop
 
        ; Получить Id процесса, чья иконка находится в трее
        invoke  GetWindowThreadProcessId,[extra.Wnd],ProcTrayId
 
        ; Снимок процессов системы
        invoke  CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
        mov     ebx,eax
 
        ; Перебрать в цикле все процессы
        mov     eax,sizeof.PROCESSENTRY32
        mov     [ProcEntry.dwSize],eax
        invoke  Process32First,ebx,ProcEntry
@@:
        cmp     eax,FALSE
        je      @f
        ; Это нужный нам процесс?
        mov     eax,[ProcEntry.th32ProcessID]
        cmp     eax,[ProcTrayId]
        je      @f
        ; Следующий процесс
        invoke  Process32Next,ebx,ProcEntry
        or      eax,eax
        jz      loc_loop
        jmp     @b
@@:
        push    eax
        ; Закрыть хэндл
        invoke  CloseHandle,ebx
        pop     eax
 
        ; Имя файла определить не удалось
        or      eax,eax
        jz      @f
 
        invoke  wsprintf,buff,mask,ProcEntry.szExeFile
        add     esp,12
 
        ; Записать имя файла в консоль
        invoke  lstrlen,buff
        invoke  WriteConsole,[stdout],buff,eax,BytesRead,NULL
@@:
        ; Все иконки обработали?
        cmp     [IconsCount],0
        ja      loc_loop
 
        ; Очистить память и ресурсы
        invoke  VirtualFreeEx,[ProcId],[lpData],0,MEM_RELEASE
        invoke  CloseHandle,[ProcId]

Описанный метод протестирован и гарантированно работает в Windows XP и Windows 7, но не работает в альтернативных оболочках типа Aston Desktop, потому что в них используются другие названия классов окон и их иерархия.

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

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

Проверка и обнаружение зависших приложений

Иногда для работы требуется определение зависших приложений, окна которых не отвечают на сообщения. Для этого есть два способа. Первый - официально документированный, через функцию SendMessageTimeOut. Особенность ее работы заключается в том, что после отправки сообщения окну она ждет ответ заданное время, и, если ответа от приложения не последовало, то возвращает FALSE. Вот пример использования функции. Нужные константы, как обычно, в FASM не определены, пришлось брать их из других источников.

        ...
        ; Определить таймаут 50 миллисекунд
        TIMEOUT = 50
        ; Определить константу SMTO_ABORTIFHUNG
        SMTO_ABORTIFHUNG = 2
        ; hwnd - хэндл проверяемого окна
        invoke  SendMessageTimeout,[hwnd],NULL,0,0,SMTO_ABORTIFHUNG,TIMEOUT,NULL
        ; Если вернулся 0, то приложение "висит"
        or      eax,eax
        jz      app_hung_up
        ...

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

Второй способ - недокументированный. В Windows есть особые функции, которые используются, например, во встроенном Диспетчере задач. Для операционных систем Windows 2000 и выше это IsHungAppWindow, а для систем Windows 9x и Windows ME - функция IsHungThread (она вообще никак не документирована на MSDN). Разница между ними в том, что IsHungAppWindow работает с хэндлом окна, а IsHungThread с хэндлом потока окна. Поскольку в системной библиотеке user32.dll одновменно может быть только одна из этих функций, то включать их в секцию импорта нельзя, иначе ваше приложение вылетит с ошибкой. Также есть информация, что они вообще недоступны через экспорт. Стало быть воспользуемся штатными функциями API GetModuleHandle и GetProcAddress, но предварительно в секции данных определим нужные нам переменные:
section '.data' data readable writeable
        ...
hUser   dd ?    ; Хэндл библиотеки user32.dll
hIHW    dd ?    ; Адрес функции IsHungAppWindow (Win2k и выше)
hIHT    dd ?    ; Адрес функции IsHungThread (Win9x)
 
strUser db      'user32.dll',0
strIHW  db      'IsHungAppWindow',0
strIHT  db      'IsHungThread',0
        ...
Перед использованием надо получить все необходимые адреса функций:
Code (Assembler) : Убрать нумерацию
section '.code' code readable executable
        ...
        mov     [hIHW],0          ; Инициализировать переменные
        mov     [hIHT],0
 
        ; Получить хэндл библиотеки user32.dll
        invoke  GetModuleHandle,strUser
        mov     [hUser],eax
 
chk_WinNT:
        ; Попытаться получить адрес функции IsHungAppWindow
        invoke  GetProcAddress,[hUser],strIHW
        or      eax,eax
        jz      chk_Win9X
        mov     [hIHW],eax        ; Сохранить адрес функции
        jmp     @f
 
chk_Win9X:
        ; Попытаться получить адрес функции IsHungThread
        invoke  GetProcAddress,[hUser],strIHT
        or      eax,eax
        jz      loc_fatal_error
        mov     [hIHT],eax        ; Сохранить адрес функции
        jmp     @f
 
loc_fatal_error:
        ; По какой-то немыслимой причине не найдено ни одной функции 
        ...
@@:
        ; Продолжить обработку
        ...

И теперь сама функция проверки окна приложения на предмет его зависания. Для большей универсальности я дополнил ее кодом первого документированного способа, он будет вызываться в случае, когда не определена ни одна из функций второго способа. Если время выполнения функции имеет критическое значение, например, при частом обновлении большого списка процессов или окон, то предпочтительнее воспользоваться именно этим ее вариантом.
;----------------------------------------------------------------
; Процедура проверки окна на зависание его родительского приложения
; Предварительно должны быть определены переменные hIHW или hIHT
; с адресами функций IsHungAppWindow или IsHungThread
;
; На входе:
;   hwnd - хэндл проверяемого окна
; На выходе:
;   EAX = 0 - приложение работает или такое окно не найдено
;   EAX = 1 - приложение зависло и не отвечает
;----------------------------------------------------------------
proc IsAppHung hwnd:dword
        local   tmp:DWORD     ; Временная переменная
 
        pusha
 
        ; Такое окно есть?
        invoke  IsWindow,[hwnd]
        or      eax,eax
        jz      .loc_ret
.chk_WinNT:
        ; Адрес функции IsHungAppWindow известен?
        cmp     [hIHW],0
        je      .chk_Win9x
        stdcall [hIHW],[hwnd]
        jmp     .loc_ret
.chk_Win9x:
        ; Адрес функции IsHungThread известен?
        cmp     [hIHT],0
        je      .chk_All
        invoke  GetWindowThreadProcessId,[hwnd],NULL
        stdcall [hIHT],eax
        jmp     .loc_ret
.chk_All:
        ; Определить таймаут 50 миллисекунд
        TIMEOUT = 50
        ; Определить константу SMTO_ABORTIFHUNG
        SMTO_ABORTIFHUNG = 2
        invoke  SendMessageTimeout,[hwnd],NULL,0,0,SMTO_ABORTIFHUNG,TIMEOUT,NULL
        sub     eax,1
        neg     eax
.loc_ret:
        ; Сохранить значение
        mov     [tmp],eax
 
        ; Восстановить регистры и сохраненное значение
        popa
        mov     eax,[tmp]
 
        ret
endp

Если очень частый вызов проверки не требуется, то код инициализации можно включить в саму функцию IsAppHung. В результате получится универсальная и самодостаточная функция, не требующая для работы вообще никаких сторонних переменных.
;----------------------------------------------------------------
; Процедура проверки окна на зависание его родительского приложения
;
; На входе:
;   hwnd - хэндл проверяемого окна
; На выходе:
;   EAX = 0 - приложение работает или такое окно не найдено
;   EAX = 1 - приложение зависло и не отвечает
;----------------------------------------------------------------
proc IsAppHung hwnd:dword
        local   tmp:DWORD     ; Временная переменная
 
        pusha
 
        ; Такое окно есть?
        invoke  IsWindow,[hwnd]
        or      eax,eax
        jz      .loc_ret
 
        ; Получить хэндл библиотеки user32.dll
        invoke  GetModuleHandle,strUser
        mov     [tmp],eax
 
.chk_WinNT:
        invoke  GetProcAddress,[tmp],strIHW
        or      eax,eax
        jz      .chk_Win9x
        stdcall eax,[hwnd]
        jmp     .loc_ret
.chk_Win9x:
        invoke  GetProcAddress,[tmp],strIHT
        or      eax,eax
        jz      .chk_All
        mov     [tmp],eax
        invoke  GetWindowThreadProcessId,[hwnd],NULL
        stdcall [tmp],eax
        jmp     .loc_ret
.chk_All:
        ; Определить таймаут 50 миллисекунд
        TIMEOUT = 50
        ; Определить константу SMTO_ABORTIFHUNG
        SMTO_ABORTIFHUNG = 2
        invoke  SendMessageTimeout,[hwnd],NULL,0,0,SMTO_ABORTIFHUNG,TIMEOUT,NULL
        sub     eax,1
        neg     eax
.loc_ret:
        ; Сохранить значение
        mov     [tmp],eax
 
        ; Восстановить регистры и сохраненное значение
        popa
        mov     eax,[tmp]
 
        ret
 
strUser db      'user32.dll',0
strIHW  db      'IsHungAppWindow',0
strIHT  db      'IsHungThread',0
 
endp

По результатам полевых испытаний функции выявлены некоторые интересные факты. В частности выполнялся полный перебор всех top-level окон с проверкой их на предмет зависания. В процессе перебора на рабочей системе Windows XP были пойманы два системных окна с неизменными заголовками "M" и "Default IME", которые определялись как зависшие. Однако это не так, просто эти окна принадлежат к критическим системным процессам. Поэтому если ваша программа также выполняет проверку всех окон, то эти два окна в обработчике зависших процессов надо будет просто пропускать после проверки их заголовка.

Для имитации зависшего приложения можно воспользоваться программой Bad Application или программой hangup.exe из архива. В архиве примеры программ для обнаружения зависших приложений всеми тремя способами и программа-имитатор зависшего приложения. Имитаторы зависших приложений придется снимать только через менеджер процессов.

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

Как узнать, что программа запущена под Администратором

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

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

; Сегмент данных
section '.data' data readable writeable
 
SECURITY_NT_AUTHORITY       = 5
TOKEN_READ                  = 0x00020008
SECURITY_BUILTIN_DOMAIN_RID = 0x00000020
DOMAIN_ALIAS_RID_ADMINS     = 0x00000220
TokenGroups                 = 0x00000002
 
BUFF_SIZE = 1024h ;  Размер буфера для групп доступа токена
 
NtAuthority     db 0,0,0,0,0,SECURITY_NT_AUTHORITY
 
hTokenHandle    dd ?
dInfoSize       dd ?
psidAdmins      dd ?
hHeap           dd ?
pTokenGroups    dd ?
 
;---------------------------------------------
 
; Сегмент кода
section '.code' code readable executable
        ...
        ; Получить токен текущего процесса
        invoke  GetCurrentProcess
        invoke  OpenProcessToken,eax,TOKEN_READ,hTokenHandle
 
        ; Выделить память для массива групп
        invoke  GetProcessHeap
        mov     [hHeap],eax
 
        invoke  HeapAlloc,eax,HEAP_ZERO_MEMORY,BUFF_SIZE
        mov     [pTokenGroups],eax
 
        ; Получить информацию о группах доступа токена
        invoke  GetTokenInformation,[hTokenHandle],TokenGroups,\
                [pTokenGroups],dword BUFF_SIZE,dInfoSize
 
        ; Прибраться за собой
        invoke  CloseHandle,[hTokenHandle]
 
        invoke  AllocateAndInitializeSid,NtAuthority,2,\
                SECURITY_BUILTIN_DOMAIN_RID,\
                DOMAIN_ALIAS_RID_ADMINS,0,0,0,0,0,0,psidAdmins
 
        ; Количество записей в структуре TOKEN_GROUPS
        mov     esi,[pTokenGroups]
        mov     ebx,dword [esi]
        ; Указатель на массив SID_AND_ATTRIBUTES
        add     esi,4
@@:
        ; Проверить соответствие SID
        mov     eax,dword [esi]
        invoke  EqualSid,[psidAdmins],eax
        or      eax,eax
        jnz     loc_admin
 
        ; Следующая группа
        add     esi,8
        dec     ebx
        or      ebx,ebx
        jnz     @b
 
loc_not_admin:
        ; Пользователь не Администратор
        ...
 
loc_admin:
        ; Пользователь Администратор
        ...

Обратите внимание, что никаких проверок на ошибки не выполняется, оставлен только рабочий код. Полный вариант вы можете посмотреть в приложении к статье.

Второй вариант очень похож на предыдущий, но будет работать только на системах, начиная с Windows 2000. В нем используется функция CheckTokenMembership, которая и выполняет все громоздкие проверки.
; Сегмент данных
section '.data' data readable writeable
 
SECURITY_NT_AUTHORITY       = 5
SECURITY_BUILTIN_DOMAIN_RID = 0x00000020
DOMAIN_ALIAS_RID_ADMINS     = 0x00000220
 
NtAuthority     db 0,0,0,0,0,SECURITY_NT_AUTHORITY
 
psidAdmins      dd ?
pbAdmin         dd ?
 
;---------------------------------------------
 
; Сегмент кода
section '.code' code readable executable
        ...
        invoke  AllocateAndInitializeSid,NtAuthority,2,\
                SECURITY_BUILTIN_DOMAIN_RID,\
                DOMAIN_ALIAS_RID_ADMINS,0,0,0,0,0,0,psidAdmins
 
        invoke  CheckTokenMembership,NULL,[psidAdmins],pbAdmin
        mov     eax,[pbAdmin]
        ; Если EAX=1, то программа запущена под Администратором
        ...

Следующий вариант самый короткий, но он также будет работать только на новых системах. В нем используется функция IsUserAdmin из библиотеки setupapi.dll. Как вы можете догадаться из ее названия, результатом работы этой функции будет TRUE, если пользователь является Администратором, и FALSE, если нет.
        ...
        invoke  IsUserAdmin
        ; Если EAX=1, то программа запущена под Администратором
        ...

И последний способ, не совсем обычный, заключается в том, что сперва мы получаем логин текущего пользователя, а затем с помощью функции NetUserGetInfo запрашиваем подробную информацию о нем (структура USER_INFO_1). В поле usri1_priv хранится информация о правах доступа этого пользователя.
; Сегмент данных
section '.data' data readable writeable
 
struct  USER_INFO_1
        usri1_name         dd ?
        usri1_password     dd ?
        usri1_password_age dd ?
        usri1_priv         dd ?
        usri1_home_dir     dd ?
        usri1_comment      dd ?
        usri1_flags        dd ?
        usri1_script_path  dd ?
ends
 
dSize           dd 100h
szUname         rb 100h
info            dd ?
 
NERR_SUCCESS    = 0
USER_PRIV_ADMIN = 2
 
;---------------------------------------------
 
; Сегмент кода
section '.code' code readable executable
        ...
        ; Получить логин текущего пользователя
        invoke  GetUserName,szUname,dSize
 
        ; Получить информацию о пользователе
        invoke  NetUserGetInfo,NULL,szUname,1,info
        cmp     eax,NERR_SUCCESS
        jne     loc_error
 
        ; Указатель на структуру USER_INFO_1
        mov     eax,[info]
 
        ; Пользователь админ?
        cmp     dword [eax+USER_INFO_1.usri1_priv],USER_PRIV_ADMIN
        je      loc_admin
 
loc_not_admin:
        ; Пользователь не Администратор
        ...
 
loc_admin:
        ; Пользователь Администратор
        ...

Обратите внимание, что все функции, строки и другие ресурсы, использованные в последнем примере, должны быть юникодными. Также это очень ненадежный способ проверки, например, на домашнем компьютере под Windows 7 она работает нормально, а на работе под Windows XP функция NetUserGetInfo возвращает ошибку, что пользователя не существует.

Ну а в заключении еще один небольшой пример, не совсем относящийся к теме статьи, но очень близкий. Это проверка, загружена система в безопасном режиме или в нормальном.
        ; Получить информацию о загрузке системы
        invoke  GetSystemMetrics,SM_CLEANBOOT
        ; EAX=0 - нормальная загрузка
        ; EAX=1 - безопасный режим
        ; EAX=2 - безопасный режим с поддержкой сети

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

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

Запись в архивы RAR, ZIP и ARJ без помощи архиватора

Решил вспомнить свою юность, когда я занимался разработкой "самоходного программного обеспечения". Одной из особенностей моих творений было распространение не только через исполняемые файлы, но и через архивы различных форматов. Технология внедрения в архивные файлы не нова, и использовалась в разных вирусах еще со времен MS-DOS. Так как готовых решений на тот момент у меня не было, пришлось доходить до всего самому. Никакого вредоносного кода на этом сайте не появится, а вот некоторыми своими наработками по внедрению в архивы я с удовольствием поделюсь. Поскольку полноценно продублировать алгоритмы сжатия архиваторов в столь малом размере файла не представляется возможным, внедряться в архивы мы будем по методу "Store". Это означает, что файл в архив добавлен с опцией "без сжатия". Внутренние форматы различных архивных файлов, естественно, различаются, но у всех обязательно присутствуют служебные заголовки, используемые архиваторами, и, собственно, сами упакованные данные.

Самый простой для внедрения - формат RAR, используемый одноименным архиватором. Подробную спецификацию формата RAR можно почитать здесь.

;---------------------------------------------
; RAR Header
;---------------------------------------------
rhcrc   dw      ?     ; --> Low-word CRC32 of fields in header
rhtype  db      ?     ; Header type: 0x74
rhflag  dw      ?     ; Bit flags
rhsize  dw      ?     ; File header full size
rcsize  dd      ?     ; Compressed file size
rosize  dd      ?     ; Uncompressed file size
rhoss   db      ?     ; Target OS version
rfcrc   dd      ?     ; --> File CRC32
rdtm    dd      ?     ; File Time/Date
runp    db      ?     ; Archive version to extract
rmeth   db      ?     ; Packing method (store)
rnsize  dw      ?     ; File name size
rfattr  dd      ?     ; File attributes
rfname  rb      (?)   ; File name

Мнемокода "(?)" в FASM нет, просто таким образом я обозначил текстовую строку для записи имени файла неопределенной длины. В реальном проекте будет достаточно MAX_PATH или вообще фиксированного размера.
; "Хвост" архива - признак окончания данных
tail    db      0C4h,03Dh,07Bh,00h,040h,07h,00h
tail_length     = $-tail

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

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

1. rhtype = 74h - тип заголовка (файл);
2. rhflag = 8000h - флаги;
3. runp = 14h - версия архиватора, необходимая для распаковки файла;
4. rmeth = 30h - метод упаковки файла (store);
5. rfattr = 20h - атрибуты файла (архивный);
6. rhoss = 2 - целевая ОС (Windows);
7. rdtm = время и дата создания файла в формате MS-DOS;
8. rfcrc = CRC32 оригинального файла;
9. rosize = размер оригинального файла;
10. rcsize = размер сжатого файла, равен оригинальному размеру;
11. rfname = имя файла;
12. rnsize = длина имени файла;

После этого считаем CRC32 заголовка, начиная от поля rhtype и до rfname включительно, затем надо взять младшее слово от результата расчета CRC32 и записать его в поле rhcrc. Все, заголовок заполнен. Открываем целевой архив для записи, и устанавливаем указатель на позицию 7 байт от конца файла, чтобы удалить "хвост" архива. Записываем наш заголовок, следом записываем внедряемый файл. После этого записываем 7-байтовый "хвост" архива. Точно таким же способом можно прицепляться к RAR-SFX архивам. На этом теорию внедрения в архивы формата RAR можно считать освоенной.

Архиватор ARJ во времена MS-DOS фактически являлся стандартом архивирования, но сейчас утратил актуальность. Однако наработки по внедрению в него остались. Есть официальная коммерческая версия и бесплатная с открытым кодом, обе они совместимы между собой и используют одинаковый формат архива. Техническая спецификация формата ARJ есть в файлеTECHNOTE.TXT из дистрибутива коммерческой версии. Дописывание файла к архиву ARJ делается немного сложнее, чем к RAR, но тоже не представляет больших трудностей.
;---------------------------------------------
; ARJ Header
;---------------------------------------------
marker  dw      ?     ; Header ID
bhsize  dw      ?     ; --> Basic header size (acrc-fhsize)
fhsize  db      ?     ; --> First header size (afname-fhsize)
anum    db      ?     ; Archive version number
anum2   db      ?     ; Archive version to extract
osver   db      ?     ; Target OS version
aflag   db      ?     ; No any flags
ameth   db      ?     ; Archive method (0 - stored)
aftype  db      ?     ; File type (0 - binary)
ares    db      ?     ; Reserved
dtm     dd      ?     ; Date/Time last modification
csize   dd      ?     ; Compressed size
osize   dd      ?     ; Original size
crc     dd      ?     ; --> Original file CRC32
fspec   dw      ?     ; Filespec position in filename
faccess dw      ?     ; File access mode
hstdata dw      ?     ; Host data
extra1  dd      ?     ; Extended file position
edtma   dd      ?     ; Date-time accessed
edtmc   dd      ?     ; Date-time created
extra2  dd      ?     ; Original file size even for volumes
afname  rb      (?)   ; File name (ASCIIZ)
acomm   db      ?     ; File comment (ASCIIZ)
acrc    dd      ?     ; --> Basic header CRC32
ehsize  dw      ?     ; Extended header size

; "Хвост" архива - признак окончания данных
tail    db      060h,0EAh,00h,00h
tail_length     = $-tail

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

1. marker = 0EA60h - маркер заголовка архива;
2. anum = 6 - версия архиватора;
3. anum2 = 1 - версия архиватора для извлечения файла;
4. osver = 11 - ОС для запуска файла (Windows);
5. fhsize = 2Eh - размер первого заголовка от поля fhsize до afname;
6. dtm = время и дата создания файла в формате MS-DOS;
7. crc = CRC32 оригинального файла;
8. osize = размер оригинального файла;
9. csize = размер сжатого файла, равен оригинальному размеру;
10. fname = имя файла в формате ASCIIZ;

После этого надо посчитать размер полученного заголовка от поля fhsize до acommвключительно и вычислить его CRC32. Размер записывается в поле bhsize, а CRC32 в полеacrc. Остальные поля для нас значения не имеют и заполняются нулевыми значениями. Если интересно, можете почитать про них в документации. Открываем целевой архив для записи, и устанавливаем указатель на позицию 4 байта от конца файла, чтобы удалить "хвост" архива, как и в случае с форматом RAR. Записываем наш заголовок, за ним внедряемый файл и 4-байтовый "хвост" архива. Все, с форматом ARJ разобрались. Также не забывайте проверять флаги в главном заголовке архива, чтобы не повредить запароленные, многотомные и другие архивы, в которые нельзя добавлять файлы таким способом. Эти проверки сделайте самостоятельно.

Наиболее сложным для внедрения является формат ZIP. Здесь информация о сжатом файле хранится в нескольких местах: локальном заголовке и так называемой "центральной директории". Это сделано специально, чтобы можно было максимально быстро получить доступ к структуре архива, не просматривая целиком его содержимое. И именно из-за этой особенности при внедрении нам придется обрабатывать весь архив. Для больших архивов придется создавать временный файл, а в нашем случае вполне можно прочитать архив целиком в память. Спецификацию формата ZIP можно почитать на офсайте разработчиков. Этот же формат имеют архивы JAR, по сути это просто переименованные ZIP-файлы.
;---------------------------------------------
; ZIP-header (local)
;---------------------------------------------
zlid    dw      ?     ; Header Id
zlsig   dw      ?     ; Signature
zlvneed dw      ?     ; Version Need
zlflags dw      ?     ; Flags
zlmeth  dw      ?     ; Method
zldtm   dd      ?     ; DateTime
zlfcrc  dd      ?     ; --> CRC32
zlcsize dd      ?     ; Compressed Size
zlosize dd      ?     ; Uncompressed Size
zlnsiz  dw      ?     ; Size of Filename
zlefild dw      ?     ; Size of Extra Field
zlfname rb      (?)   ; Filename

;---------------------------------------------
; ZIP-header (central directory)
;---------------------------------------------
zid     dw      ?     ; Header Id
zsig    dw      ?     ; Signature
zverm   dw      ?     ; Version Made
zvneed  dw      ?     ; Version Need
zflags  dw      ?     ; Flags
zmeth   dw      ?     ; Method
zdtm    dd      ?     ; TimeDate
zfcrc   dd      ?     ; --> CRC32
zcsize  dd      ?     ; Compressed Size
zosize  dd      ?     ; Uncompressed Size
znsiz   dw      ?     ; Size of Filename
zefield dw      ?     ; Size of Extra Field
zcomm   dw      ?     ; Comment Size
zdnumb  dw      ?     ; Disk Number
ziattr  dw      ?     ; Internal Attributes
zeattr  dd      ?     ; External Attributes
zohead  dd      ?     ; Offset Header
zfname  rb      (?)   ; Filename

;---------------------------------------------
; ZIP-header (End of central directory)
;---------------------------------------------
zeid    dw      ?     ; Header Id
zesig   dw      ?     ; Signature
zenumd  dw      ?     ; Number of this disk
zenume  dw      ?     ; Number of this disk
zetotal dw      ?     ; Total number of entries of disk
zedir   dw      ?     ; Total number of entries of the central directory
zesizec dd      ?     ; Size of the central directory
zeoffs  dd      ?     ; Offset of start of central directory
zecomm  dw      ?     ; ZIP file comment length

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

1. zlid = 'PK' - сигнатура заголовка;
2. zlsig = 0403h - тип заголовка - локальный;
3. zlvneed = 10h - версия архиватора для извлечения файла;
4. zlflags = 80h - флаги файла (см. в документации);
5. zlmeth = 0 - метод упаковки без сжатия;
6. zldtm = время и дата создания файла в формате MS-DOS;
7. zlfcrc = CRC32 оригинального файла;
8. zlcsize = размер сжатого файла, равен оригинальному размеру;
9. zlosize = размер оригинального файла;
10. zlnsiz = длина имени файла;
11. zlfname = имя файла;

Центральная директория:

1. zid = 'PK' - сигнатура заголовка;
2. zsig = 0201h - тип заголовка - центральная директория;
3. zverm = 10h - версия архиватора для извлечения файла
4. zvneed = 10h - версия архиватора для извлечения файла;
5. zflags = 80h - флаги файла (см. в документации);
6. zmeth = 0 - метод упаковки без сжатия;
7. zdtm = время и дата создания файла в формате MS-DOS;
8. zfcrc = CRC32 оригинального файла;
9. zcsize = размер сжатого файла, равен оригинальному размеру;
10. zosize = размер оригинального файла;
11. znsiz = длина имени файла;
12. zeattr = 20h - атрибут файла (архивный)
13. zfname = имя файла;

После заполнения заголовков переносим упакованные файлы из исходного архива в новый. Для этого в цикле проверяем сигнатуру локальных заголовков с самого начала архива. Если она равна 0x04034b50, то вычисляем размер блока по сумме размера локального заголовка от поляzlid до zlfname, длины имени файла zlnsiz и длины дополнительных полей zlefild. Когда все упакованные файлы из исходного архива будут перенесены в новый, можно записывать в новый архив наш заполненный локальный заголовок и тело файла. Перед этим надо запомнить абсолютное смещение нашего локального заголовка файла относительно начала нового архива и заполнить этим значением поле zohead заголовка нашего файла в центральной директории. Архив может содержать дополнительные данные (Archive Extra Data), они расположены после файлов и оформлены своим заголовком с сигнатурой 0x08064b50. Формат и значения полей заголовка описаны в документации. Их также надо записать в новый архив. Теперь надо перенести все записи из центральной директории исходного архива в новый. Каждый блок идентифицируется по заголовку 0x02014b50, размер блока равен сумме размера заголовка центральной директории от поля zid до поля zfname, длины имени файла znsiz и длины дополнительных данных zefield. Перед переносом центральной директории надо также запомнить ее абсолютное смещение относительно начала нового архива, это значение потребуется при заполнении поля zeoffs завершающей структуры центральной директории (End of central directory record). Когда центральная директория исходного файла перенесена в новый архив, можно дописывать к ней заголовок центральной директории нашего файла. После этого в новый архив надо перенести завершающую структуру центральной директории, она начинается с сигнатуры 0x06054b50. Предварительно в ней надо увеличить на 1 значения полей, обозначающих количество файлов в архиве (zenumd и zenume) и размер центральной директории (увеличить текущее значение zesizec на размер заголовка нашего файла). Полный размер блока завершающей структуры центральной директории равен сумме размера ее заголовка от поля zeid до поля zecomm включительно и размера комментария архива zecomm. Вот, вроде бы и все. Если из описания процесс обработки ZIP-архивов не очень понятен, то смотрите исходники. Как вариант, можно обрабатывать содержимое архива не с начала файла, а сразу анализируя заголовки центральной директории. Такой способ вполне имеет право на существование и, наверное, может даже считаться более надежным.

UPD: При написании статьи я столкнулся с некоторыми JAR-архивами, у которых в локальном заголовке размеры файла и CRC32 были просто обнулены, в результате этого невозможно посчитать размер блока файла. Видимо программы, работающие с ними, ориентируются на данные центральной директории. Попытка внедриться в такие архивы описанным выше способом приведет к их безвозвратному повреждению, поэтому при этом способе внедрения такие файлы надо пропускать. Я разобрался со способом внедрения в архив через центральную директорию. Здесь алгоритм немного другой. Сперва надо найти End of central directory record. Она имеет фиксированный размер, но архив также может содержать комментарий, который записан ПОСЛЕ End of central directory record в конце файла, а размер комментария записан в нее же. Так что я не придумал ничего лучше, чем просто сканировать файл с конца по сигнатуре0x06054b50, благо что максимальный размер комментария не может превышать значения WORD, то есть 65535 байт. Из найденной структуры мы извлекаем данные о местоположении центральной директории относительно начала файла. После этого начинаем по очереди перебирать записи из нее, а именно смещение упакованных файлов и их размеры, записывая их в новый архив. Здесь важно учитывать следующий момент: если в локальном заголовке файла в поле zlflags установлен 3-й бит, то информация о контрольной сумме, а также о размерах упакованного и оригинального файла содержатся в 16-байтном блоке data descriptor, который записан сразу же после упакованного файла и начинается с сигнатуры 0x08074b50. Причем в архиве могут одновременно быть файлы как с заполненными локальными заголовками, так и с неполными заголовками + data descriptor. Таким образом, мы через центральную директорию находим файл в архиве, записываем его в новый архив, проверяем наличие data descriptor, в случае его наличия также переносим в новый архив сразу после файла. Когда будут перенесены все файлы, записываем наш файл. После этого записываем оригинальную центральную директорию, данные нашего файла из центральной директории и скорректированную End of central directory record с файловым комментарием (при его наличии). Смотрите исходники, там понятнее. Таким образом можно корректно прицепляться к обычным архивам, JAR-архивам и даже документам OpenOffice (odt, ods) и MS Office 2010 (docx, xlsx), которые по сути также являются ZIP-архивами. Кроме того, при обработке центральной директории можно внедряться не только в конец, но и в середину архива, а также подменять собой уже имеющиеся в архиве файлы. Но это уже переходит границу добрых дел, поэтому останется только идеей.

Не забывайте обрабатывать многотомные и защищенные архивы (данные о шифровании хранятся в отдельном блоке). Еще есть 64-битные версии ZIP-архивов, они имеют несколько другой формат заголовков и не могут быть обработаны как обычные ZIP-архивы. Повторюсь, что подробное описание всех служебных заголовков и форматов можно найти в официальной документации.

К сожалению, современные архиваторы типа 7zip, WinRK, KuaiZip, WinArchiver и WinUHA имеют более сложный внутренний формат, там даже имена файлов подвергаются сжатию или хранятся в отдельном блоке, а KGB Archiver вообще не подразумевает возможность модификации своих архивов.

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

Время непрерывной работы (Uptime) Windows

Для получения времени непрерывной работы системы обычно используется функция API GetTickCount. Она возвращает количество миллисекунд, прошедших с момента последнего старта системы. Проблема в том, что счетчик имеет тип dword, и по прошествии примерно 50 дней (49,7 если быть точным) достигает предельного значения и обнуляется. Конечно, продержать систему без перезагрузки почти два месяца трудно, но не значит что невозможно. Поэтому для получения гарантированно точного времени работы системы воспользуемся функцией NtQuerySystemInformation. Достаточно долго эта функция относилась к разряду недокументированных, теперь же на MSDN по ней имеется описание с примечанием, что ее использование в прикладных программах все равно нежелательно.

; Сегмент данных
section '.data' data readable writeable
 
; По умолчанию структура в FASM не определена, сделаем это самостоятельно
; Для получения необходимой информации нужны только два первых значения
struct SYSTEM_TIME_INFORMATION
       liKeBootTime       dq ?  ; Время старта системы
       liKeSystemTime     dq ?  ; Текущее время
       liExpTimeZoneBias  dq ?
       uCurrentTimeZoneId dd ?
       dwReserved         dw ?
ends
 
SystemTime   SYSTEM_TIME_INFORMATION  ; Наша структура с данными
 
; Константа нужного класса информации тоже не определена, сделаем это сами
GET_SYSTEM_TIME_INFORMATION = 3 
 
; Сегмент кода
section '.code' code readable executable
...
        invoke  NtQuerySystemInformation, GET_SYSTEM_TIME_INFORMATION,\
                SystemTime, sizeof.SYSTEM_TIME_INFORMATION, 0
        ; Записать в регистры EDX:EAX текущее время в миллисекундах
        mov     eax, dword [SystemTime.liKeSystemTime]
        mov     edx, dword [SystemTime.liKeSystemTime+4]
        ; Вычесть время старта системы
        sub     eax, dword [SystemTime.liKeBootTime]
        sbb     edx, dword [SystemTime.liKeBootTime+4]
        ; Теперь в регистры EDX:EAX записано реальное количество миллисекунд,
        ; прошедшее с момента старта системы
...

Преобразовать миллисекунды в обычный вид даты и времени можно при помощи пары стандартных функций FileTimeToLocalFileTime и FileTimeToSystemTime.
; Сегмент данных
section '.data' data readable writeable
 
; По умолчанию структура в FASM не определена, сделаем это самостоятельно
; Для получения необходимой информации нужны только два первых значения
struct SYSTEM_TIME_INFORMATION
       liKeBootTime       dq ?  ; Время старта системы
       liKeSystemTime     dq ?  ; Текущее время
       liExpTimeZoneBias  dq ?
       uCurrentTimeZoneId dd ?
       dwReserved         dw ?
ends
 
SystemTime   SYSTEM_TIME_INFORMATION  ; Наша структура с данными
 
; Добавляются две стандартные структуры для работы с датой и временем
ftSystemTime FILETIME
stSystemTime SYSTEMTIME
 
; Константа нужного класса информации тоже не определена, сделаем это сами
GET_SYSTEM_TIME_INFORMATION = 3 
 
; Сегмент кода
section '.code' code readable executable
...
        invoke  NtQuerySystemInformation, GET_SYSTEM_TIME_INFORMATION,\
                SystemTime, sizeof.SYSTEM_TIME_INFORMATION, 0
        ; Получить текущее системное время. Для получения времени старта
        ; системы замените в вызове SystemTime.liKeSystemTime на
        ; SystemTime.liKeBootTime
        invoke  FileTimeToLocalFileTime, SystemTime.liKeSystemTime,\
                ftSystemTime
        ; Преобразовать в системный формат даты и времени
        invoke  FileTimeToSystemTime, ftSystemTime, stSystemTime
 
        ; Теперь к элементам структуры stSystemTime можно обращаться
        ; обычными способами: [stSystemTime.wDay], [stSystemTime.wMonth] и т.д.
...

После выполнения этого кода структура stSystemTime (стандартная структура SYSTEMTIME) содержит все значения в удобном для доступа и обработки виде.

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