Статья DEP и ASLR – игра без правил

Со времени выхода в свет Win2000 прошло уже 20 лет. Её разработчики, с чувством исполненного долга предвкушали себе счастливую старость, но грянул гром – система оказалась дырявой как сито, и на данный момент счёт её обновлениям потеряла наверное уже и сама Microsoft. Ознакомиться с установленными на вашем узле заплатками можно в командной строке, запросив systeminfo. Каждый сервис-пак SP несёт в себе кучу таких обновлений, игнорировать которым (с точки зрения безопасности) глупо.

На скорую руку заштопав старые щели, Билли сбрасывает на рынок эту-же систему под новым именем, которую ждёт та-же участь и она бумерангом прилетает обратно – круг замыкается. А виной тому хакеры, огромной ступнёй годзиллы втирающие в грязь мастдай только потому, что за свои честно/заработанные требуют товар соответствующего качества – мол если предлагаешь обществу весчь, она должна быть без изъянов. По большому счёту, только хакеры и двигают прогресс компьютерной безопасности, заставляя корпорации строить более мощные обороны.

Большие надежды Microsoft возлагала на появившейся в XP механизм DEP – Data Execute Prevension (защита от исполнения данных), который как и следовало ожидать, с треском провалился. Тогда на Win7 была предпринята попытка рандомной смены "базы загрузки образа в память" ASLR – Advanced Space Layout Randomization и вроде дела поправились. Но позже выяснилось, что и ASLR пробивается с любого расстояния.

В данной статье предлагаю пощупать технологии DEP и ASLR руками – что они из себя представляют, какие имеют слабые и сильные стороны, можно-ли их обойти на программном уровне и т.п. Попытка пройти сквозь эту стену не зная основной идеи ни к чему хорошему не приведёт, поэтому как обычно – начнём с теории.
---------------------------

"Data Execute Protect" и виртуальная память

Защита от исполнения данных DEP, может быть как аппаратной, так и программной. Программная модель (software-enforced) используется только на 32-битных системах Win, в то время как аппаратно защищать данные от исполнения (hardware-enforced) могут исключительно 64-битные процессоры, с операционной системой такой-же разрядности. Связано это с тем, что фактически DEP привязан к биту 63 в записях виртуальных страниц PTE – Page Table Entry, а в 32-битных системах этого бита попросту нет. Поэтому на х32 его приходится эмулировать программным способом, подключая для этих дел отдельное ядро Ntkrnlpa.exe.

Когда в файле boot.ini установлен флаг /РАЕ, система загружается с указанного альтернативного ядра, а дефолтное Ntoskrnl.exe отправляется на скамейку запасных (оба ядра лежат в папке \windows\system32). РАЕ означает "Physical Address Extension", или расширение физического адреса. В этом режиме, 32-битному процессору становится доступным уже не 4 Gb адресного пространства, а все 64. На рисунке показаны фундаментальные признаки трансляции адреса на различных ядрах 32-битной оси (фрагменты позаимствованы из мануалов AMD том.2):

Ntoskrnl.png


Значит имеем "каталог виртуальных страниц" PDT, на который указывает регистр управления CR3 текущего процесса (см.левый скрин PDE). В этом каталоге хранятся 10-бит=1024 записей Entry размером dword (32-бита). Соответственно каждая такая запись PDE указывает на одну из 1024 таблиц PT, в которой так-же 1024 записи PTE (10-бит). И наконец записи PTE – PageTableEntry – указывают на одну из 1024 страниц виртуальной памяти размером 4Кб (12-бит), что в сумме даёт максимум 1024*1024*4096=4Gb памяти. Младшие 12-бит адреса выбирает уже смещение внутри данной страницы, двигая окно вверх или вниз.

Теперь посмотрим на правый рисунок..
Основное отличие РАЕ-ядра в том, что увеличилась разрядность всех записей Entry с 32 до 52-бит, зато уменьшилось общее их кол-во с 1024 до 512 (было 10-бит, стало 9). Дополнительные разряды в записях позволяют создавать вдвое больше каталогов PDT и таблиц PT, в результате чего на выходе получаем гигантское число страниц виртуальной памяти, и каждая страница по 4Кб. Есть ещё и другие режимы работы транслятора, в которых страницы имеют размер не 4Кб, а 2 или 4М-байт. Однако сути это не меняет, поэтому рассматривать их не будем – кому интересно, курите маны разработчиков от Intel (том.3) или AMD (том.2).

Когда записи PTE 32-битные, в них нет лишнего места и они забиты инфой под-завязку. Но при увеличении разрядности до 64-бит (52 из которых полезные), остаётся много свободных, и в них можно закодировать что-то ещё. Формат записей PTE обоих ядер и зарытый в них глубокий смысл, представлен на рисунке ниже:

paePTE.png


Из приведённого рисунка видно, что в режиме РАЕ запись стала в два раза длиннее, что позволило в самом старшем бите (63) спрятать бит защиты от исполнения NX. Другими словами, запись PTE в таблице РТ хранит базовый адрес одной из страниц виртуальной памяти, и если в этой записи бит (63) установлен в единицу, то описываемая данной записью страница будет защищена от исполнения – в лучшем случае она будет доступна только на r/w. Здесь так-же видно, что non-PAE ядро ntoskrnl.exe не имеет этого бита, поэтому DEP на этом ядре не может существовать в принципе. Это относится к 32-битной линейке Win-XP без РАЕ.

nonPae.png


В отличии от систем х32, на 64-битных системах всё проще и РАЕ-ядро им ни к чему – его просто нет в составе ОС, и система работает в т.н. режиме AWE – Address Windowing Extensions (расширение адресного окна). Регистры и все структуры этих процессоров изначально 64-битные и трансляция виртуального адреса (которым мы оперируем в своих программах) происходит по следующей схеме:

LongMode.png


Как видим, хоть адрес и 64-битный, но используются только 48-бит из них. Такая разрядность позволяет адресовать память размером 2^48=256 терабайт, чего с избытком хватит для любой программы. Кстати по аналогии с виртуальной памятью х64, адресация накопителей HDD/SSD тоже 48-битная, но это уже из другой кухни, т.к. шины и контроллёры у них разные. Это так.. чисто к сведению..


Системная поддержка DEP

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

1. AlwaysOff = 0
Аппаратный DEP отключён для всех частей системы, и мы можем отправить свой шелл хоть в стек, хоть в кучу, хоть к чёрту на кулички. То-есть ОС работает на стандартном ядре Ntoskrnl.exe без РАЕ.​
2. AlwaysOn = 1
Аппаратный DEP включён для всех, и его (!)нельзя отключить для избранных приложений. Все процессы выполняются только с включенным DEP. Любые исправления игнорируются, что предвещает нам полные кранты.​
3. OptInt = 2
Аппаратный DEP автоматически включается только для компонентов ОС. Няшно.. и это дефолт для клиентских версий Win. Мы можем явно включить/отключить его программным способом для выбранных приложений, или текущего своего процесса.​
4. OptOut = 3
DEP автоматически включается для всех процессов, включая пользовательские – это дефолт для серверных версий Win. Как и в предыдущей конфигурации, DEP можно явно отключить для любых приложений. Исправлены ошибки совместимости систем.​
Судя по этому списку, воспринимать DEP как крутую защиту нельзя. Думаю молчаливая договорённость уже созрела в наших головах, а если нет, то вот некоторые нюансы.. Мало того, что DEP'ом можно программно оперировать, так ещё в запазухе у нас имеется функция всех времён и народов VirtualProtect(Ex), которая с лёгкостью переназначает любые атрибуты страниц виртуальной памяти, внося изменения в их записи PTE.

Здесь настораживает только конфиг под номером 2 и если честно, то его приходится воспринимать как суровую действительность. Если в ответ на запрос мы получим значение "AlwaysOn=1", значит любые попытки обойти защиту будут заранее обречены на провал.

Но этот выхлоп скорее исключение, чем правило и может обламать нас только на серверных системах, которые мы сейчас не берём в счёт. Дело в том, что во всех клиентских версиях, DEP отключают на время и легальные программы типа распаковщики, архиваторы, всякого рода визарды и близкие им по смыслу тулзы. Поэтому мастдай включает эту опцию редко, хотя вполне может создать и список исключений. Так-что процент попаданий можно определить только подбросив монетку вверх, а дальше – на чьё ухо сядет муха.

Основные пользовательские функции для работы с DEP хранятся в библиотеке kernel32.dll и могут использоваться на прикладном уровне с админскими правами – вот их список:

1. GetSystemDEPPolicy() - запрашивает параметр системной политики DEP.
Эта функция не имеет аргументов и возвращает в регистр EAX одно из четырёх указанных выше значений 0,1,2,3. Общесистемная политика настраивается во время загрузки ОС - изменить её можно только пропатчив boot.ini (смотри хелп в конфигураторе BCD по маске "NX"). На глобальном уровне, DEP активируется битом (11) MSR-регистра 0xC0000080 под названием IA32_EFER.NXE (no_execute_enable, см.маны Интела том.4).

2. GetProcessDEPPolicy() - запрашивает параметр DEP для указанного процесса.
Эта функция BOOL и возвращает TRUE при успешной операции, иначе FALSE=0.

C-подобный:
GetProcessDEPPolicy()
  hProcess       ;// дескриптор запрашиваемого процесса (нуль для своего),
  lpFlags        ;// указатель на переменную для флагов,
  lpPermanent    ;// указатель на переменную для возможных операций.
                 ;// если Permanent = TRUE, то политику изменить нельзя!
;// значения флагов
;//------------------------
;// 0 = DEP отключён для указанного процесса,
;// 1 = DEP включён для указанного процесса,
;// 2 = Эмуляция DEP-ALT отключёна для указанного процесса.

3. SetProcessDEPPolicy() - устанавливает параметры DEP для указанного процесса.
Функция имеет всего один аргумент в виде одного из трёх флагов, предыдущей функции Get(). На положительный результат влияет в первую очередь общесистемная политика DEP, которая должна быть "OptIn" или "OptOut". Во-вторых функция проверки GetProcessDEPPolicy() должна перманентом возвратить FALSE, что будет означать возможность оперировать с DEP.

Напишем небольшую утилиту, которая будет вызывать функции из этого списка.
Значит сначала берём системный статус, потом статус для своего процесса и выводим всю информацию на экран. Строки выводит будем используя "таблицы переходов", чтобы исключить допотопные проверки значения в цикле CASE:

C-подобный:
format   pe console
include  'win32ax.inc'
entry    start
;//------
.data
capt     db   13,10,' DEP info v0.1'
         db   13,10,' ~~~~~~~~~~~~~~~~',0
sysDep   db   13,10,' System status: %x = %s ',0
softDep  db   13,10,' MyProc DEP '
         db   13,10,'     Status...: %x = %s ',0
perm     db   13,10,'     Permanent: %x = %s ',0

table1   dd   dOff,dOn,optIn,optOut               ;// таблица указателей на строки
dOff     db   'AlwaysOff, Hard disable',0
dOn      db   'AlwaysON, Hard enable',0
optIn    db   'OptIn, Soft enable system only',0
optOut   db   'OptOut, Soft enable for all',0

table2   dd   sOff,sOn,sEmu
sOff     db   'Disable',0
sOn      db   'Enable',0
sEmu     db   'EMU Disable',0

table3   dd   pOff,pOn
pOff     db   'FALSE, changeable',0
pOn      db   'TRUE, not changeable',0

flags    dd   0
lpPerm   dd   0
frmt     db   '%s',0

;//------
.code
start: cinvoke  printf,capt

;// Запрашиваем системную политику
;// -------------------------------
        invoke  GetSystemDEPPolicy     ;// возвращает в EAX 0,1,2,3
        mov     esi,table1             ;// ESI = укащатель на таблицу
        push    sysDep                 ;// спецификатор для printf как аргумент для процедуры ниже
        call    PrintResult            ;// зовём самопальную процедуру вывода на экран!

;// Запрашиваем политику своего процесса
;// -------------------------------------
        invoke  GetProcessDEPPolicy,0,flags,lpPerm     ;// если EAX =0, значит ошибка.
        mov     esi,table2             ;// адрес таблицы переходов
        mov     eax,[flags]            ;// EAX = флаги текущего процесса
        push    softDep                ;// аргумент для fn.ниже
        call    PrintResult            ;//

;// Показываем, можем или нет мы изменять политику
;// -----------------------------------------------
        mov     esi,table3             ;//
        mov     eax,[lpPerm]           ;// EAX = перманент от fn. выше
        push    perm                   ;//
        call    PrintResult            ;//

@exit: cinvoke  scanf,frmt,capt        ;// ждём нажатие клавиши..
       cinvoke  exit,0                 ;// на выход!

;// Пользовательская процедура вывода на экран
;// -------------------------------------------
proc    PrintResult messageId          ;// имеет 2-аргумента (push до call, и eax)
        mov     ebx,eax                ;// EAX/EBX = значение
        shl     ebx,2                  ;// умножить число на 4 (смещение текста от начала)
        add     esi,ebx                ;// ESI = указатель на строку из таблицы переходов
       cinvoke  printf,[messageId],eax,dword[esi]   ;// выводим значение и соответствующую строку!
        ret                            ;// выход из процедуры по адресу-возврата CALL в стеке.
Endp

;//=== Секция импорта =======================
data     import
library  msvcrt,'msvcrt.dll', kernel,'kernel32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import   kernel, GetSystemDEPPolicy,'GetSystemDEPPolicy',\
                 GetProcessDEPPolicy,'GetProcessDEPPolicy'
end      data

softDep.png


А вот что возвращает этот код на моей XP с отключённым DEP и на стандартном ядре Ntoskrnl.exe, в то время-как Win-7 работает на ядре Ntkrnlpa.exe. Здесь видно, что DEP отключён на аппаратном уровне и статус возвращает "AlwaysOff=0".

hardtDep.png


Таким образом, DEP не так страшен, как его малюют. На дне его кармана имеется тунель в прошлое, и Microsoft никак не может его прикрыть. Только вдумайтесь – организовать защиту от исполнения DEP на уровне виртуальных страниц, и в это-же время оставить функцию VirtualProtectEx(), которая сводит на нет эту защиту и может снимать атрибуты не только со-своих, но и с чужих страниц. Кроме того есть и VirtualAlloc(), которая выделяет страницы с любыми атрибутами.. Какого цвета у разработчиков кровь и что они там курят – хз, нужен эксперимент.


ASLR – принцип работы механизма

Рассмотрим очередную "бомбу" под названием "Advanced Space Layout Randomization" ASLR, которая начиная с Win-7 призвана рандомно менять базу образа в памяти. Отметим, что все системные файлы Win запускаются с включённым механизмом ASLR, но у сторонних приложений он может быть отключён. Чтобы наши программы влились в эту струю, нужно указать это явно, посредством флага компиляции "/DYNAMIC_BASE".

Для образов прикладных программ, новая рандомная база вычисляется при каждом запуске исполняемого файла. В отличие от юзерских программ, базы системных образов вычисляется только один раз, при начальной загрузке операционной системы и используется во всей системе вплоть до её перезагрузки. Если-бы системные DLL заново отображались по разным адресам внутри различных процессов, их код нельзя было-бы использовать совместно. Загрузчику пришлось-бы корректировать указатели на функции API для каждого процесса в отдельности, превращая общие функции в закрытые данные процесса.

Механизм ASLR рандомно меняет базы трёх блоков памяти процесса – это: база образа, база стека, и база кучи Heap. Рандом образа - 8-битный, что позволяет выбрать одну из 256 возможных баз. А вот рандом стека и кучи уже 5-битный – такая разрядность ограничивает выбор до 32-х базовых адресов.

Все-кто хочет под управлением ASLR кочевать в памяти, должны иметь в своём РЕ-заголовке определённые флаги, которые задаются программистом при компиляции проекта. Первое поле размером 16-бит находится по-смещению РЕ.16h и называется "Characteristics". Набор этих флагов определяет глобальные свойства исполняемого файла, и каждый из 16-ти его бит несёт в себе определённую информацию. Основные его биты перечислены нихе:

• Бит 0 – IMAGE_FILE_RELOCS_STRIPPEDтаблетка от ASLR.
Устанавливается в единицу, если в файле нет информации о перемещениях, и он должен лечь на дефолтную базе. Если база занята кем-то другим, загрузчик сообщает об ошибке.​
• Бит 1 – IMAGE_FILE_EXECUTABLE_IMAGE
Устанавливается, если файл является исполняемым экзе, и действительно может быть запущен.​
• Бит 4 – IMAGE_FILE_AGGRESIVE_WS_TRIM
Если взведён, то ОС принудительно урежет объём памяти для этого процесса, путём разбиения его на страницы. Этим битом могут похвастаться процессы-демоны, которые большую часть времени спят, пробуждаясь лишь несколько раз в день.​
• Бит 5 – IMAGE_FILE_LARGE_ADDRESS_AWARE
Приложение способно работать с памятью, объёмом больше 2 Gb.​
• Бит 12 – IMAGE_FILE_SYSTEM
Если взведён, значит это системный файл типа драйвера.​
• Бит 13 – IMAGE_FILE_DLL
Взведённый бит является признаком DLL-библиотеки.​
Из всей этой группы, в контексте ASLR примечательным является только бит (0), который разрешает релоки. У библиотек DLL он всегда сброшен, а у исполняемых файлов типа *.ехе – наоборот взведён. Это потому, что при запуске процесса на исполнение сначала в память загружается экзе, который сразу занимает предпочтительную базу, и только потом уже он подтягивает в своё пространство импортируемые либы – т.е. кто первым встал, того и база. Если DLL будет неперемещаемой, то может получиться конфликт базовых адресов EXE<->DLL.

Чтобы продемонстрировать состояние этих флагов, запустим программу "РЕ-Explorer". Она не то-чтобы отображает флаги, но и позволяет редактировать их биты. Это одна из лучших утилит в своём роде, способная проникнуть в самые закрома исполняемых файлов:

char_Aslr.png


Однако это не единственный бит, от которого плящет ASLR.. а точнее - он для него второстепенный. Прямым рычагом для рандомной релокации образа является слово по-смещение РЕ.5Eh с несовпадающим по смыслу именем "DLL-characteristics". Именно в этом поле хранятся ключи, которые мы задаём компилятору при сборке своего исходника в экзе. Как упоминалось выше, чтобы системный механизм ASLR подхватил наш файл, нужно указать компилятору ключ "/DYNAMIC_BASE" – иначе образ так и останется неперемещаемым, хоть в системе и будет активен ASLR. Бит-мап 16-битного поля РЕ.5Еh представлен ниже:

dllFlags.png


C-подобный:
0x0001  Зарезерв (должно быть ноль).
0x0002  Зарезерв.
0x0004  Зарезерв.
0x0008  Зарезерв.
0x0020  IMAGE_DLL_HIGH_ENTROPY_VA = ASLR с энтропией 24 бита (для х64).
0x0040  IMAGE_DLL_DYNAMIC_BASE = ASLR с энтропией 8 бит (для х32).
0x0080  IMAGE_DLL_FORCE_INTEGRITY = обязательная проверка целостности кода.
0x0100  IMAGE_DLL_NX_COMPAT = образ совместим с NX (механизм DEP).
0x0200  IMAGE_DLL_NO_ISOLATION = не изолировать образ.
0x0400  IMAGE_DLL_NO_SEH = запрет на использование в образе обработчиков исключений SEH.
0x0800  IMAGE_DLL_NO_BIND = не связываемый образ.
0x1000  IMAGE_DLL_APPCONTAINER = образ должен выполняться в AppContainer.
0x2000  IMAGE_DLL_WDM_DRIVER = образ является драйвером WDM.
0x4000  IMAGE_DLL_GUARD_CF = поддерживает защиту Control Flow Guard, CFG.
0x8000  IMAGE_DLL_TERMINAL_SERVER_AWARE = образ является терминальным сервером.

Мда.. судя по характеристикам, адепты этой секты способны на многое, и ожидать от них можно чего угодно. Заполучив админские права, и привелегию "Debug" мы можем рекурсией обойти все файлы в системной папке и сбросить непригодные нам биты ASLR. Проблема открытых файлов и невозможности перезаписать их образ на диске, решается копированием дескрипторов открытых файлов функциями OpenProcess() с последующим DuplicateHandle(). В результате мы получим родительские права на объект, со-всеми вытекающими последствиями.

Не знаю для чего это нужно, ведь базу образа можно найти и динамическим путём. Любая малварь поколения "Next" поступает именно так, а жёсткую привязку через РЕ-заголовок можно найти сейчас только на свалке истории. Однако для самообразования можно написать такую утилиту, которая будет искать все исполняемые файлы в текущем дире жёсткого диска, и выводить информацию о поддержки ими технологии ASLR. Тут просто поиск и проверка флага "DLL_DYNAMIC_BASE" в поле РЕ.5Еh:

C-подобный:
format   pe console
include  'win32ax.inc'
entry    start
;//------
.data
capt     db   13,10,' ASLR info v0.1'
         db   13,10,' ~~~~~~~~~~~~~~~~~~~',0
info     db   13,10,10,' File: ======== %s'
         db   13,10,'       PE.16h = %04X'
         db   13,10,'       PE.5Eh = %04X',0
aslr     db   '  <-- ASLR dynamicBase bit found',0

fData    WIN32_FIND_DATA       ;// структура для FindFile()
fName    =    fData +44        ;//   ..смещение имени-файла в ней
fMask    db   '*.exe',0        ;// маска для поиска = только ЕХЕ
frmt     db   '%s',0           ;// спецификатор для scanf()
hndl     dd   0                ;// хэндл поиска
buff     rb   512              ;// буфер под РЕ-заголовок

;//------
.code
start:  cinvoke  printf,capt   ;// шапка

;//--- Поиск подходящих кандидатов.. ---------
         invoke  FindFirstFile,fMask,fData    ;// ищем первый файл
         mov     [hndl],eax                   ;// запомнить хэндл поиска

;//--- Сбрасываем заголовок найденного файла в буфер -----
@scan:   invoke  _lopen,fName,0           ;// открываем файл
         push    eax                      ;// запомнить его хэндл
         invoke  _lread,eax,buff,512      ;// считываем 512 байт в свой буфер
         pop     eax                      ;// хендл на родину
         invoke  _lclose,eax              ;// файл больше не нужен..

;//--- Поиск флагов в буфере -----------------
         mov     esi,buff                 ;// ESI = голова буфа
         mov     esi,[esi+0x3c]           ;// указатель на РЕ-заголовок
         add     esi,buff                 ;// смещаемся к нему
         mov      ax,word[esi+0x16]       ;// AX = характеристики ЕХЕ
         mov      bx,word[esi+0x5e]       ;// ВХ = характеристики DLL
         and     eax,0xffff               ;// обнулить не нужное
         and     ebx,0xffff               ;// ...^^^
         push    ebx                      ;// запомнить флаги DLL
        cinvoke  printf,info,fName,eax,ebx   ;// выводим батву на консоль

;//--- Определяем поддержку файлом ASLR ------
         pop     ebx                      ;// ЕВХ = флаги DLL
         test    ebx,1000000b             ;// чекнуть бит "DLL_DYNAMIC_BASE"
         jz      @next                    ;// если он нуль..
        cinvoke  printf,aslr              ;// иначе: даём знать об этом!

@next:   invoke  FindNextFile,[hndl],fData   ;// продолжить поиск в дире..
         or      eax,eax                     ;// если EAX = 0, нет больше файлов
         jnz     @scan                       ;// повторить, если NoZero.

         invoke  CloseHandle,[hndl]  ;// прибить хэндл поиска
 @exit: cinvoke  scanf,frmt,capt     ;// ждём клаву..
        cinvoke  exit,0              ;//

;//=== Секция импорта ========================
data     import
library  msvcrt,'msvcrt.dll',kernel,'kernel32.dll'
import   msvcrt,printf,'printf',scanf,'scanf',exit,'exit'
import   kernel,FindFirstFile,'FindFirstFileA',\
         FindNextFile,'FindNextFileA',CloseHandle,'CloseHandle',\
         _lopen,'_lopen',_lread,'_lread',_lwrite,'_lwrite',_lclose,'_lclose'
end      data

dynBase.png


По идее, чтобы прибить ASLR для избранных файлов, нужно обнаружив у него флаг "DynamicBase" сбросить его, тогда у механизма ASLR останутся несыгранные роли. Но чем он нам мешает? У рандомной смены базы тоже есть своя грация, над которой "художники" постарались на славу. М.Руссинович в седьмом издании своего бестцеллера "Внутреннее устройство Windows" описывает эти механизмы на более глубоком уровне, вплоть до алгоритмов вычисления дельты для рандома и прочее. Так-что всем, кого интересует данная тема, советую почитать этот его том. А за сим всё.. до скорого.
 

explorer

Platinum
05.08.2018
1 080
2 475
BIT
0
Твою серию статей смело можно назвать образцовой. Именно так следует другим делать - с чёткой структурой, в нужных местах акцентированное выделение цветом, картинки, сам стиль написания без всякой похабщины.
Спасибо!
 

Marylin

Mod.Assembler
Red Team
05.06.2019
326
1 451
BIT
696
Именно так следует другим делать
Спасибо за отзыв..
Как правило контекст зависит от внутреннего состояния на данный момент, поэтому стараюсь сливать своё нутро только в хорошем расположении духа, и только на трезвую голову - никакой музыки в ушах, и никакого пива. Лет 10 назад переписывал черновики по несколько раз, а сейчас уже легче, и на втором круге корректирую только ошибки в ворде.

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

Обучение наступательной кибербезопасности в игровой форме. Начать игру!