Jump to content
Sign in to follow this  
mr.save

Фундаментальные основы хакерства. Мастер-класс по анализу исполняемых файлов в IDA Pro

Recommended Posts

В этой статье мы окунемся в глубокий и подробный статический анализ с помощью IDA Pro — сверхпопулярного среди хакеров и специалистов по информационной безопасности дизассемблера. С первых же версий он занял заслуженное место лидера рынка. Это своего рода швейцарский армейский нож, которым мы будем разделывать цифровую добычу. Начнем с самого базового анализа и постепенно будем пробираться вперед, разгребая заросли кода.

С легкой руки Денниса Ритчи повелось начинать освоение нового языка программирования с создания простейшей программы «Hello, World!». Не будем нарушать эту традицию и оценим возможности IDA Pro следующим примером.

#include <iostream>
void main()
{
  std::cout << "Hello, Sailor!\n";
}

Компилятор сгенерирует исполняемый файл объемом почти 190 Кбайт, большую часть которого займет служебный, стартовый или библиотечный код. Попытка дизассемблирования с помощью таких средств, как W32Dasm, не увенчается быстрым успехом, поскольку над полученным листингом размером в два с половиной мегабайта (!) можно просидеть не час и не два. Легко представить, сколько времени уйдет на серьезные задачи, требующие изучения десятков и сотен мегабайтов дизассемблированного текста.

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

ida_begin.thumb.png.8fc198f7941be9d996499a71ee30ce7c.png

Чтобы открыть текстовое представление, надо из контекстного меню выбрать пункт Text view.

Цитата

Какую версию IDA Pro выбрать?

Последней версией IDA Pro на момент написания статьи была 7.3. Ее цена может быть великовата для покупки в исследовательских целях. Как известно, Ильфак Гильфанов очень строго относится к утечкам и появлению продуктов своей компании в интернете и не допускает подобного.

Однако на сайте компании Hex-Rays в публичный доступ выложена бесплатная версия дизассемблера с функциональными ограничениями. Например, она не получает обновления после достижения майлстоуна целой версии, то есть сейчас для свободной загрузки доступна версия 7.0. Также она поддерживает только архитектуры x86 и x64.

Тем не менее этого вполне достаточно для наших целей. Потому что нам не придется разбираться в коде для процессоров ARM, Motorola, Sparc, MIPS или Zilog. Еще одно ограничение накладывается на использование в коммерческих целях, но и тут наша совесть чиста.

Закончив автоматический анализ файла first.exe, IDA переместит курсор к строке .text:0040628B — точке входа в программу. Не забудь из графического режима отображения листинга переключиться в текстовый. Также обрати внимание на строчку .text:00406290 start endp ; sp-analysis failed, выделенную красным цветом в конце функции start. Так IDA отмечает последнюю строку функции в случае, если у нее в конце return и значение указателя стека на выходе из функции отличается от такового на входе.

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

  • argc — количество аргументов командной строки;
  • argv — массив указателей на строки аргументов;
  • _environ — массив указателей на строки переменных окружения.

Заполняется структура OSVERSIONINFOEX, которая среди прочего включает:

  • dwBuildNumber — билд;
  • dwMajorVersion — старшую версию операционной системы;
  • dwMinorVersion — младшую версию операционной системы;
  • _winver — полную версию операционной системы;
  • wServicePackMajor — старшую версию пакета обновления;
  • wServicePackMinor — младшую версию пакета обновления.

Далее Start инициализирует кучу (heap) и вызывает функцию main, а после возвращения управления завершает процесс с помощью функции Exit. Для получения значений структуры OSVERSIONINFOEX используется функция GetVersionEx.

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

CRtO.demo.c

#include <stdio.h>
#include <stdlib.h> 
#include <Windows.h>

void main()
{
  OSVERSIONINFOEX osvi;
  ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
  osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
  GetVersionEx((OSVERSIONINFO*)&osvi);

  int a;
  printf(">OS Version:\t\t\t%d.%d\n\
  >Build:\t\t\t%d\n\
  >Arguments count:\t%d\n", \
    osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwOSVersionInfoSize, __argc);
  for (a = 0; a < __argc; a++)
    printf(">\t Argument %02d:\t\t%s\n", a + 1, __argv[a]);
  a = !a - 1;
  while (_environ[++a]);
    printf(">Environment variables count:%d\n", a);
  while (a) 
    printf(">\tVariable %d:\t\t%s\n", a, _environ[--a]);
}

Прототип функции main как будто указывает, что приложение не принимает никаких аргументов командной строки, но результат работы программы говорит об обратном и на моей машине выглядит так (приводится в сокращенном виде):

OS Version: 6.2

Build: 156

Arguments count: 1

Argument 01: CRt0.demo.exe

Environment variables count: 99

...

Variable 20: FrameworkVersion=v4.0.30319

Variable 19: FrameworkDIR32=C:\WINDOWS\Microsoft.NET\Framework\

Variable 18: FrameworkDir=C:\WINDOWS\Microsoft.NET\Framework\

Variable 17: Framework40Version=v4.0

Variable 16: ExtensionSdkDir=C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs

Variable 15: DriverData=C:\Windows\System32\Drivers\DriverData

Очевидно, нет никакой необходимости анализировать стандартный стартовый код приложения, и первая задача исследователя — найти место передачи управления на функцию main. К сожалению, чтобы гарантированно решить эту задачу, потребуется полный анализ содержимого функции Start. У исследователей есть много хитростей, которые позволяют не делать этого, но все они базируются на особенностях реализации конкретных компиляторов и не могут считаться универсальными.

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

Рекомендую ознакомиться с исходниками стартовых функций популярных компиляторов. Для Visual C++ 14 в соответствии с архитектурой они находятся в подпапках каталога %\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\. Их изучение упростит анализ дизассемблерного листинга.

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

//******************** Program Entry Point ********************

:0040628B E802070000 call 00406992

:00406290 E974FEFFFF jmp 00406109
* Referenced by a CALL at Addresses:

|:0040380C , :004038B2 , :0040392E , :00403977 , :00403A8E

|:00404094 , :004040FA , :00404262 , :00404BF4 , :00405937

|:004059AE

|

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:004062B6(U)

|

:00406295 8B4DF4 mov ecx, dword ptr [ebp-0C]

:00406298 64890D00000000 mov dword ptr fs:[00000000], ecx

:0040629F 59 pop ecx

:004062A0 5F pop edi

:004062A1 5F pop edi

:004062A2 5E pop esi

:004062A3 5B pop ebx

:004062A4 8BE5 mov esp, ebp

:004062A6 5D pop ebp

:004062A7 51 push ecx

:004062A8 F2 repnz

:004062A9 C3 ret

...

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

Поэтому способности дизассемблера тесно связаны с его версией и полнотой комплекта поставки — далеко не все версии IDA Pro в состоянии работать с программами, сгенерированными современными компиляторами.

.text:0040628B start proc near

.text:0040628B

.text:0040628B ; FUNCTION CHUNK AT .text:00406109 SIZE 00000127 BYTES

.text:0040628B ; FUNCTION CHUNK AT .text:00406265 SIZE 00000026 BYTES

.text:0040628B

.text:0040628B call sub_406992

.text:00406290 jmp loc_406109

.text:00406290 start endp ; sp-analysis failed

.text:00406290

.text:00406295 ; [00000015 BYTES: COLLAPSED FUNCTION __EH_epilog3. PRESS CTRL-NUMPAD+ TO EXPAND]

.text:004062AA ; [00000011 BYTES: COLLAPSED FUNCTION __EH_epilog3_GS. PRESS CTRL-NUMPAD+ TO EXPAND]

.text:004062BB ; [00000034 BYTES: COLLAPSED FUNCTION __EH_prolog3. PRESS CTRL-NUMPAD+ TO EXPAND]

.text:004062EF ; [00000037 BYTES: COLLAPSED FUNCTION __EH_prolog3_GS. PRESS CTRL-NUMPAD+ TO EXPAND]

.text:00406326 ; [00000037 BYTES: COLLAPSED FUNCTION __EH_prolog3_catch. PRESS CTRL-NUMPAD+ TO EXPAND]

.text:0040635D

.text:0040635D ; =============== S U B R O U T I N E ===============

.text:0040635D

.text:0040635D ; Attributes: thunk

.text:0040635D

.text:0040635D sub_40635D proc near ; CODE XREF: sub_4042FD+19↑p

.text:0040635D jmp sub_406745

.text:0040635D sub_40635D endp

.text:0040635D

.text:00406362

...

Перечень поддерживаемых компиляторов можно найти в файле %IDA%/SIG/list. В нем есть старинные Microsoft C и Quick C, Visual C++ с первой по восьмую версию и Visual.Net. А вот Visual C++ 14 из Visual Studio 2017 здесь нет. Однако, взглянув в окно IDA, мы видим, что дизассемблер сумел определить многие (но не все) функции.

Заглянем в окно вывода, находящееся внизу. Там, немного прокрутив вывод, мы обнаружим строчку Using FLIRT signature: SEH for vc7-14, говорящую о том, что используемая версия IDA все же понимает компиляторы Visual C++ от 7 до 14.

ida_begin_text.thumb.png.ca1c16bfcc064ffafc6aa3594dff9701.png

Давай разбираться в получившемся листинге. Первое и в данном случае единственное, что нам надо найти, — это функция main. В начале стартового кода после выполнения процедуры sub_406992 программа совершает прыжок на метку loc_406109:

.text:0040628B start proc near

.text:0040628B call sub_406992

.text:00406290 jmp loc_406109

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

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

В итоге доходим до вызова функции: call sub_4010D0. Похоже, это и есть функция main, поскольку здесь дизассемблер смог распознать строковую переменную и дал ей осмысленное имя aHelloSailor, а в комментарии, расположенном справа, для наглядности привел оригинальное содержимое Hello, Sailor!\n. Смещение этой строки компилятор закинул на вершину стека, а затем ниже через строчку, по всей видимости, происходит вызов функции вывода на экран:

.text:004010D3 push offset aHelloSailor ; "Hello, Sailor!\n"

.text:004010D8 push offset unk_42DE30

.text:004010DD call sub_401170

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

Если поместить курсор в границы имени aHelloSailor и нажать Enter, IDA автоматически перейдет к строке, в которой выполняется определение переменной:

.rdata:0041E1A0 aHelloSailor db 'Hello, Sailor!',0Ah,0 ; DATA XREF: sub_4010D0+3↑o

Выражение DATA XREF: sub_4010D0+3↑o называется перекрестной ссылкой и свидетельствует о том, что в третьей строке процедуры sub_4010D0 произошло обращение к текущему адресу по его смещению (o от слова offset), а стрелка, направленная вверх, указывает на относительное расположение источника перекрестной ссылки.

Если навести курсор на выражение sub_4010D0+3↑o и нажать Enter, то IDA Pro перейдет к следующей строке:

.text:004010D3 push offset aHelloSailor ; "Hello, Sailor!\n"

Нажатие Esc отменяет предыдущее перемещение, возвращая курсор в исходную позицию.

К слову, дизассемблер W32Dasm даже не смог распознать строковую переменную.

Цитата

 

Что не так с IDA?

Положа руку на сердце — я был слегка разочарован, ибо ожидал, что IDA распознает больше библиотечных процедур. Поэтому я решил натравить «Иду» на такую же программу, но сгенерированную более ранней версией компилятора. Подопытным кроликом выступил Visual C++ 8.0 (VS 2005).

Сравним результаты работы компиляторов. Тот же исходник, компиляция из командной строки (папка first05). Загрузим итоговый экзешник в «Иду». Листинг приводится в сокращенном виде для экономии пространства.

list-1.thumb.png.4b5607cc9133f34cd4fa87d70d6bad2d.png

Мало того что стартовый код меньше по объему, так еще было автоматически определено большее количество библиотечных функций, среди которых: GetVersionExA, GetProcessHeap, HeapFree и ряд других. Среди них достаточно просто найти вызов main и перейти на саму функцию.

Тем не менее VC++ 8.0 — ушедшая эпоха, и в пример я ее привел только для ознакомления.

 

На этом анализ приложения first.cpp можно считать завершенным. Для полноты картины остается переименовать функцию sub_4010D0 в main. Для этого подводи курсор к строке .text:004010D0 (началу функции) и жми N. В появившемся диалоге можешь ввести main. Результат должен выглядеть так:

.text:004010D0 ; int __cdecl main(int argc, const char **argv, const char **envp)

.text:004010D0 main proc near ; CODE XREF: start-8D↓p

.text:004010D0

.text:004010D0 argc = dword ptr 8

.text:004010D0 argv = dword ptr 0Ch

.text:004010D0 envp = dword ptr 10h

.text:004010D0

.text:004010D0 push ebp

.text:004010D1 mov ebp, esp

.text:004010D3 push offset aHelloSailor ; "Hello, Sailor!\n"

.text:004010D8 push offset unk_42DE30

.text:004010DD call sub_401170

.text:004010E2 add esp, 8

.text:004010E5 xor eax, eax

.text:004010E7 pop ebp

.text:004010E8 retn

.text:004010E8 main endp

Обрати внимание: IDA в комментарии подставила прототип функции, а ниже параметры по умолчанию.

 

IDA и зашифрованные программы

Другое важное преимущество IDA — способность дизассемблировать зашифрованные программы. В примере Crypt00.com используется статическое шифрование, которое часто встречается в «конвертных» защитах. Между тем этот файл не запустится в Windows 10, поскольку *.com для работы требует 16-разрядную исполняемую среду.

Я думаю, это не повод отказаться от анализа столь интересного примера, тем более что существуют мощные средства виртуализации, поэтому поставить 32-битную Windows XP, которая выполняет 16-битные проги, — не проблема. К тому же анализ файлов .com значительно проще, чем .exe, так как первые сильно короче вторых.

Мы уже видели непроходимые заросли библиотечного кода, вставленного компилятором в минимальной exe-программе, тогда как в com все по минимуму, в чем мы скоро убедимся. Отмечу также, что последней версией IDA Pro, работающей в 32-разрядных средах, была 6.8.

sourcer.png.7211afc399a0ce6b0507410f811c1096.png

Рассматриваемый прием шифрования полностью «ослепляет» большинство дизассемблеров. Например, результат обработки файла Crypt00.com при помощи Sourcer выглядит так:

crypt00 proc far

3A6A:0100 start:

3A6A:0100 ъBE 010D mov si,10Dh ; (3A6A:010D=0C3h)

3A6A:0103 loc_1: ; xref 3A6A:010B

3A6A:0103 80 34 77 xor byte ptr [si],77h ; 'w'

3A6A:0106 46 inc si

3A6A:0107 81 FE 0124 cmp si,124h

3A6A:010B 76 F6 jbe loc_1 ; Jump if below or =

3A6A:010D C3 retn

3A6A:010E 7E CD 62 76 BA 56 db 7Eh,0CDh, 62h, 76h,0BAh, 56h

3A6A:0114 B4 3F 12 1B 1B 18 db 0B4h, 3Fh, 12h, 1Bh, 1Bh, 18h

3A6A:011A 5B 57 20 18 05 13 db 5Bh, 57h, 20h, 18h, 05h, 13h

3A6A:0120 56 7A 7D 53 db 56h, 7Ah, 7Dh, 53h

crypt00 endp

Самостоятельно Sourcer не сумел дизассемблировать половину кода, оставив ее в виде дампа. С другой стороны, как-то помочь ему мы не можем. Напротив, IDA изначально проектировалась как дружественная к пользователю интерактивная среда. В отличие от Sourcer-подобных дизассемблеров, IDA не делает никаких молчаливых предположений и при возникновении затруднений обращается за помощью к человеку. Результат анализа «Идой» файла Crypt00.com выглядит так:

seg000:0100 public start

seg000:0100 start proc near

seg000:0100 mov si, 10Dh

seg000:0103

seg000:0103 loc_10103: ; CODE XREF: start+B↓j

seg000:0103 xor byte ptr [si], 77h

seg000:0106 inc si

seg000:0107 cmp si, 124h

seg000:010B jbe short loc_10103

seg000:010D retn

seg000:010D start endp

seg000:010D

seg000:010D ; -----------------------------------------

seg000:010E db 7Eh, 0CDh, 62h, 76h, 0BAh, 56h, 0B4h, 3Fh, 12h, 2 dup(1Bh)

seg000:010E db 18h, 5Bh, 57h, 20h, 18h, 5, 13h, 56h, 7Ah, 7Dh, 53h

seg000:010E seg000 ends

seg000:010E

seg000:010E

seg000:010E end start

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

 

Данные

Что представляет собой число 10Dh в строке 0x100 — константу или смещение? Очевидно, в регистр SI заносится смещение, потому что впоследствии операнд по этому смещению в памяти интерпретируется как байт и над ним выполняется операция XOR.

Чтобы преобразовать константу в смещение, установи текстовый курсор на 10Dh и нажми O. Дизассемблируемый текст станет выглядеть так:

seg000:0100 mov si, offset locret_1010D

...

seg000:010D locret_1010D: ; DATA XREF: start+B↑o

seg000:010D retn

seg000:010D start endp

seg000:010D

seg000:010D ; -----------------------------------------

seg000:010E db 7Eh, 0CDh, 62h, 76h, 0BAh, 56h, 0B4h, 3Fh, 12h, 2 dup(1Bh)

seg000:010E db 18h, 5Bh, 57h, 20h, 18h, 5, 13h, 56h, 7Ah, 7Dh, 53h

seg000:010E seg000 ends

IDA Pro автоматически создала новое имя locret_1010D, которое ссылается на зашифрованный блок кода. Попробуем преобразовать его в данные. Для этого надо поставить курсор на строку 010D и дважды нажать D, чтобы утвердительно ответить на вопрос во всплывающем диалоге. Листинг примет следующий вид:

...

seg000:010D word_1010D dw 7EC3h ; DATA XREF: start↑o

seg000:010F db 0CDh ; =

seg000:0110 db 62h ; b

seg000:0111 db 76h ; v

seg000:0112 db 0BAh ; ¦

seg000:0113 db 56h ; V

seg000:0114 db 0B4h ; +

seg000:0115 db 3Fh ; ?

seg000:0116 db 12h

seg000:0117 db 1Bh

seg000:0118 db 1Bh

seg000:0119 db 18h

seg000:011A db 5Bh ; [

seg000:011B db 57h ; W

seg000:011C db 20h

seg000:011D db 18h

seg000:011E db 5

seg000:011F db 13h

seg000:0120 db 56h ; V

seg000:0121 db 7Ah ; z

seg000:0122 db 7Dh ; }

seg000:0123 db 53h ; S

seg000:0123 seg000 ends

Но на что именно указывает word_1010D? Понять это позволит изучение следующего кода:

seg000:0100 start proc near

seg000:0100 mov si, offset word_1010D

seg000:0103

seg000:0103 loc_10103: ; CODE XREF: start+B↓j

seg000:0103 xor byte ptr [si], 77h

seg000:0106 inc si

seg000:0107 cmp si, 124h

seg000:010B jbe short loc_10103

seg000:010B start endp

После того как в регистр SI попадает смещение, начинается цикл, который представляет собой простейший расшифровщик: значение в регистре SI указывает на символ, команда XOR с помощью числа 0x77 расшифровывает один байт (один ASCII-символ). Напомню, в ассемблере запись шестнадцатеричных чисел вида 77h. После этого инкрементируется значение регистра SI (указатель переводится на следующий символ) и получившееся значение сравнивается с числом 0x124, которое равно общему количеству символов для расшифровки.

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

Давай расширим сегмент подопытной программы. Жмем Shift-F7 (View → Open subviews → Segments), откроется вкладка Program Segmentation. В контекстном меню единственного существующего сегмента seg000 выбираем пункт Edit Segments (Ctrl-E). В диалоге в поле ввода End address введи значение побольше, например 0x10125. Подтверди свое намерение в появившемся диалоге.

change_seg.png.d5819f0c93f65b13ecc838333e2069a5.png

Можешь полюбоваться на увеличившийся сегмент. Вернемся к изучению кода. Если в результате сравнения значение в регистре SI меньше общего количества байтов или равно ему, выполняется переход на метку loc_10103 и блок кода повторяется для расшифровки следующего байта. Отсюда можно заключить, что word_1010D указывает на начало последовательности байтов для расшифровки. Подведя к ней курсор, жмем N и можем дать ей осмысленное имя, например BeginCrypt. А константу 124h можем сначала преобразовать в смещение (Ctrl-O), а затем переименовать, например в EndCrypt.

 

Расшифровка

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

На практике, однако, это выглядит несколько иначе. Прежде чем расшифровывать, необходимо выяснить алгоритм расшифровки, проанализировав доступную часть файла. Затем нужно выйти из дизассемблера, тем или иным способом расшифровать «секретный» фрагмент, вновь загрузить файл в дизассемблер (причем предыдущие результаты работы дизассемблера окажутся утерянными) и продолжить его анализ до тех пор, пока не встретится еще один зашифрованный фрагмент. Теперь цикл «выход из дизассемблера — расшифровка — загрузка — анализ» повторяется вновь.

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

Слева каждой строки указывается имя сегмента и его смещение, например seg000:0103. Однако нам нужно другое значение. Установив текстовый курсор на нужную строку, смотри на нижнюю часть текущей вкладки (полагаю, у тебя это IDA View-A).

ida_bottom.png.b9a2a5542c9fd29537b1d421acc005c1.png

При перемещении курсора соответствующее смещение тоже меняется (на рисунке выше оно обведено рамочкой). С его помощью можно обратиться к любой ячейке сегмента. Для чтения и модификации ячеек предусмотрены функции Byte и PatchByte соответственно. Их вызов может выглядеть, например, так: a=Byte(0x01010D) читает ячейку, расположенную по смещению 0x01010D; PatchByte(0x01010D,0x27) присваивает значение 0x27 ячейке памяти, расположенной по смещению 0x01010D. Как следует из названия функций, они манипулируют ячейками размером в один байт.

Знания языка C и этих двух функций вполне достаточно для написания скрипта-расшифровщика.

Реализация IDA-С не полностью придерживается стандарта, в частности IDA не позволяет разработчику задавать тип переменной и определяет его автоматически по ее первому использованию, а объявление осуществляется ключевым словом auto. Например, auto MyVar, s0 объявляет две переменные — MyVar и s0.

Для создания скрипта необходимо нажать Shift-F2 или выбрать в меню File пункт Script Command. В результате откроется окно Execute script. Большую его часть занимают список скриптов и поле ввода для редактирования выбранного скрипта.

Дополнительно внизу окна находятся ниспадающий список для выбора используемого языка (IDC или Python), ниспадающий список для указания размера табуляции и четыре кнопки: Run (выполнить скрипт), Export (экспортировать скрипт в файл), Import (загрузить скрипт из файла) и Save (сохранить скрипт в базу данных проекта).

После первого открытия окна в списке скриптов по умолчанию выбран скрипт Default snippet. В качестве его тела введем такой код:

auto a, x;
for (a = 0x01010D; a <= 0x010123; a++) {
  x = Byte(a);
  x = (x ^ 0x77);
  PatchByte(a, x);
  Message(x);
}

idc_decrypt.png.54aaba241c80ebe6463d2162b8b425c2.png

Как было показано выше, алгоритм расшифровщика сводится к последовательному преобразованию каждой ячейки зашифрованного фрагмента операцией XOR 0x77:

seg000:0103 xor byte ptr [si], 77h

Сам же зашифрованный фрагмент начинается с адреса 0x01010D и продолжается вплоть до 0x010123.

В конце командой Message отправляем модифицированный символ в область вывода IDA.

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

...

seg000:010D BeginCrypt dw 9B4h ; DATA XREF: start↑o

seg000:010F db 0BAh ; ¦

seg000:0110 db 15h

seg000:0111 db 1

seg000:0112 db 0CDh ; =

seg000:0113 db 21h ; !

seg000:0114 db 0C3h ; +

seg000:0115 db 48h ; H

seg000:0116 db 65h ; e

seg000:0117 db 6Ch ; l

seg000:0118 db 6Ch ; l

seg000:0119 db 6Fh ; o

seg000:011A db 2Ch ; ,

seg000:011B db 20h

seg000:011C db 57h ; W

seg000:011D db 6Fh ; o

seg000:011E db 72h ; r

seg000:011F db 64h ; d

seg000:0120 db 21h ; !

seg000:0121 db 0Dh

seg000:0122 db 0Ah

seg000:0123 db 24h ; $

seg000:0124 EndCrypt db ? ; DATA XREF: start+7↑o

seg000:0124 seg000 ends

seg000:0124

seg000:0124

seg000:0124 end start

А в окне вывода появится надпись

ґ єН!ГHello, Word!

$

Возможные ошибки: несоблюдение регистра символов (IDA к этому чувствительна), синтаксические ошибки, неверно заданные адреса границ модифицируемого блока. В случае ошибки необходимо подвести курсор к строке seg000:010D, нажать клавишу U (для удаления результатов предыдущего дизассемблирования зашифрованного фрагмента) и затем C (для повторного дизассемблирования расшифрованного кода).

Символы перед фразой «Hello, World!» не выглядят читаемыми; скорее всего, это не ASCII, а исполняемый код. Поставим курсор на строку seg000:010D, жмем C («Преобразовать в инструкцию»). В результате листинг будет выглядеть так:

...

seg000:010D BeginCrypt: ; DATA XREF: start↑o

seg000:010D mov ah, 9

seg000:010F mov dx, 115h

seg000:0112 int 21h ; DOS - PRINT STRING

seg000:0112 ; DS:DX -> string terminated by "$"

seg000:0114 retn

seg000:0114 ; ---------------------------------------

seg000:0115 db 48h ; H

seg000:0116 db 65h ; e

seg000:0117 db 6Ch ; l

seg000:0118 db 6Ch ; l

seg000:0119 db 6Fh ; o

seg000:011A db 2Ch ; ,

seg000:011B db 20h

seg000:011C db 57h ; W

seg000:011D db 6Fh ; o

seg000:011E db 72h ; r

seg000:011F db 64h ; d

seg000:0120 db 21h ; !

seg000:0121 db 0Dh

seg000:0122 db 0Ah

seg000:0123 db 24h ; $

seg000:0124 EndCrypt db ? ; DATA XREF: start+7↑o

seg000:0124 seg000 ends

seg000:0124

seg000:0124

seg000:0124 end start

Цепочку символов, расположенную начиная с адреса seg000:0115, можно преобразовать в удобочитаемый вид, если навести на нее курсор и нажать A. Еще можно преобразовать константу 115h в строке 010F в смещение. Теперь экран дизассемблера будет выглядеть так:

...

seg000:010D BeginCrypt: ; DATA XREF: start↑o

seg000:010D mov ah, 9

seg000:010F mov dx, offset aHelloWord ; "Hello, Word!\r\n$"

seg000:0112 int 21h ; DOS - PRINT STRING

seg000:0112 ; DS:DX -> string terminated by "$"

seg000:0114 retn

seg000:0114 ; ---------------------------------------

seg000:0115 aHelloWord db 'Hello, Word!',0Dh,0Ah,'$' ; DATA XREF: seg000:010F↑o

seg000:0124 EndCrypt db ? ; DATA XREF: start+7↑o

seg000:0124 seg000 ends

seg000:0124

seg000:0124

seg000:0124 end start

Команда MOV AH, 9 в строке seg000:010D подготавливает регистр AH перед вызовом прерывания 0x21. Она выбирает функцию вывода строки на экран, а ее смещение следующей командой заносится в регистр DX. Иными словами, для успешного ассемблирования листинга необходимо заменить константу 0x115 соответствующим смещением.

Но ведь выводимая строка на этапе ассемблирования (до перемещения кода) расположена совсем в другом месте! Одно из возможных решений этой проблемы — создать новый сегмент и затем скопировать в него расшифрованный код. Это будет аналогом перемещения кода работающей программы.

 

Создание сегмента

Для создания нового сегмента надо открыть вкладку Segments (Shift-F7) и нажать Insert. Появится вот такое окно.

create_seg.png.df15ea3ad28451545ff79168736ac1c2.png

Базовый адрес сегмента может быть любым, если при этом не перекрываются сегменты seg000 и MySeg; начальный адрес сегмента задается так, чтобы смещение первого байта было равно 0x100; размер нового сегмента сделаем таким же, как seg000. Не забудь выбрать тип создаваемого сегмента: 16-битный сегмент.

Далее будем двигаться поэтапно. Сначала скопируем команды для вывода символов в консоль. Начнем брать байты со смещения 10D сегмента seg000, а вставлять — с самого начала сегмента MySeg. Это можно сделать скриптом следующего содержания:

auto a, x;
for (a = 0x0; a < 0x8; a++) {
  x = Byte(0x1010D+a);
  PatchByte(0x20100+a,x);
}

Для его ввода необходимо вновь нажать комбинацию клавиш Shift-F2. Создать еще один скрипт можно нажатием Insert. После выполнения экран дизассемблера будет выглядеть так (показано только начало сегмента MySeg):

MySeg:0100 ; Segment type: Regular

MySeg:0100 MySeg segment byte public '' use16

MySeg:0100 assume cs:MySeg

MySeg:0100 ;org 100h

MySeg:0100 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing

MySeg:0100 db 0B4h ; +

MySeg:0101 db 9

MySeg:0102 db 0BAh ; ¦

MySeg:0103 db 15h

MySeg:0104 db 1

MySeg:0105 db 0CDh ; =

MySeg:0106 db 21h ; !

MySeg:0107 db 0C3h ; +

MySeg:0108 db ? ;

Надо преобразовать данные в инструкции: поставить курсор на строку MySeg:0100 и нажать C. Листинг примет ожидаемый вид:

MySeg:0100 mov ah, 9

MySeg:0102 mov dx, 115h

MySeg:0105 int 21h ; DOS - PRINT STRING

MySeg:0105 ; DS:DX -> string terminated by "$"

MySeg:0107 retn

Чтобы программа-клон вела себя немного не так, как ее родитель, добавим ожидание ввода символа. Для этого надо поставить курсор на команду retn и выбрать Edit → Patch program → Assemble...

Введи XOR AX, AX, нажми Enter. Затем INT 16h, снова Enter. Последняя инструкция — RET, Enter и Esc для закрытия диалога.

xor.png.40f545557487c811cad4c5e260ca6944.png

Теперь с помощью следующего скрипта скопируем байты, составляющие текст «Hello, World!»:

auto a, x, i;
i = 0;
for (a = 0x0115; a < 0x124; a++) {
  x = Byte(0x10000+a);
  PatchByte(0x2010C+i,x);
  i++;
}

Поставив курсор на строку MySeg:010C, нажимаем A и преобразуем цепочку символов в удобочитаемый вид. В строке MySeg:0102 надо изменить константу 115h на фактическое значение, по которому расположена фраза для вывода: MySeg:010C. Для этого ставь курсор на указанную строку и открывай диалог Assemble Instruction (Edit → Patch program → Assemble...) и вводи MOV DX, 10Ch.

Теперь надо преобразовать константу 10Ch в смещение, а последовательность символов, расположенную по нему, обратить к светскому виду. Как это делать, ты уже знаешь. Напоследок рекомендую произвести косметическую чистку — уменьшить размер сегмента до необходимого. Чтобы удалить адреса, оставшиеся при уменьшении размеров сегмента за его концом, поставь флажок Disable Address в окне свойств сегмента.

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

  • mov si, offset BeginCrypt в сегменте seg000;
  • cmp si, offset EndCrypt в сегменте seg000;
  • mov dx, offset aHelloWord_0 в сегменте MySeg.

Между тем после ассемблирования символьные имена будут заменены числовыми константами. Будут ли в таком случае в результирующей программе эти смещения указывать на те же вещи, что в исходной? Давай еще раз проанализируем наш листинг. Первое смещение, BeginCrypt, указывает на строку seg000:010D. По сути, весь предыдущий код будет скопирован, поэтому ее значение менять не нужно. Второе смещение, EndCrypt, указывающее на конец сегмента, должно увеличиться на четыре байта, так как мы добавили две инструкции:

seg000:0114 xor ax, ax

seg000:0116 int 16h

Чтобы подсчитать их размер, достаточно из следующего за ними смещения вычесть их начальное: 118h – 114h = 4h байта. В результате EndCrypt должно указывать на 124h + 4h = 128h. Установи курсор на строку seg000:0107, вызови окно ассемблера и замени инструкцию в ней на cmp si, 128h.

Третье смещение, aHelloWord_0, в исходной программе равно 10Ch. Подумаем, как должен измениться адрес. Если aHelloWord_0 находится в сегменте MySeg, то нам просто нужно прибавить к имеющемуся смещению размер расшифровщика в сегменте seg000. Его можно посчитать как разность начального смещения зашифрованного блока и начального адреса: 0x010D – 0x0100 = 0xD байт.

В итоге смещение aHelloWord_0 должно указывать на 0x10C + 0xD = 0x119. Изменим код: установив курсор на строку MySeg:0102, с помощью встроенного ассемблера модифицируем ее содержимое на mov dx, 119h.

В результате полный листинг выглядит следующим образом:

seg000:0100 ; Segment type: Pure code

seg000:0100 seg000 segment byte public 'CODE' use16

seg000:0100 assume cs:seg000

seg000:0100 org 100h

seg000:0100 assume es:nothing, ss:nothing, ds:seg000, fs:nothing, gs:nothing

seg000:0100

seg000:0100 ; =========== S U B R O U T I N E ================

seg000:0100

seg000:0100

seg000:0100 public start

seg000:0100 start proc near

seg000:0100 mov si, offset BeginCrypt

seg000:0103

seg000:0103 loc_10103: ; CODE XREF: start+Bj

seg000:0103 xor byte ptr [si], 77h

seg000:0106 inc si

seg000:0107 cmp si, 128h

seg000:010B jbe short loc_10103

seg000:010B start endp

seg000:010B

seg000:010D

seg000:010D BeginCrypt: ; DATA XREF: starto

seg000:010D mov ah, 9

seg000:010F mov dx, offset aHelloWord ; "Hello, Word!\r\n$"

seg000:0112 int 21h ; DOS - PRINT STRING

seg000:0112 ; DS:DX -> string terminated by "$"

seg000:0114 retn

seg000:0114 ; ------------------------------------------------

seg000:0115 aHelloWord db 'Hello, Word!',0Dh,0Ah,'$' ; DATA XREF: seg000:010Fo

seg000:0124 EndCrypt db ?

seg000:0124 seg000 ends

seg000:0124

MySeg:0100 ; ------------------------------------------------

MySeg:0100

; ===========================================================

MySeg:0100

MySeg:0100 ; Segment type: Regular

MySeg:0100 MySeg segment byte public '' use16

MySeg:0100 assume cs:MySeg

MySeg:0100 ;org 100h

MySeg:0100 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing

MySeg:0100 mov ah, 9

MySeg:0102 mov dx, 119h

MySeg:0105 int 21h ; DOS - PRINT STRING

MySeg:0105 ; DS:DX -> string terminated by "$"

MySeg:0107 xor ax, ax

MySeg:0109 int 16h ; KEYBOARD - READ CHAR FROM BUFFER, WAIT IF EMPTY

MySeg:0109 ; Return: AH = scan code, AL = character

MySeg:010B retn

MySeg:010B ; ------------------------------------------------

MySeg:010C aHelloWord_0 db 'Hello, Word!',0Dh,0Ah,'$'

MySeg:011B db ? ;

MySeg:011B MySeg ends

MySeg:011B

MySeg:011B

MySeg:011B end start

Создание клона

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

Для решения этой задачи напишем скрипт, автоматически выполняющий копирование и шифрование необходимых частей программы:

auto a, f, x;
// Открывается файл crypt01.com для записи в двоичном режиме
f = fopen("crypt01.com", "wb");
// Копируется расшифровщик
for (a = 0x10100; a < 0x1010D; a++) {
  x = Byte(a);
  fputc(x, f);
}
// Копируется и на лету шифруется весь сегмент MySeg
for (a = SegStart(0x20100); a != SegEnd(0x20100); a++) {
  x = Byte(a);
  x = (x ^ 0x77);
  fputc(x, f);
}
// Файл закрывается
fclose(f);

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

 

Итоги

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

HelloWord.png.b1a325dc074930cf5390eb7e8622a947.png

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

 

Дизассемблер и отладчик в связке

Дизассемблер, включенный в отладчик, обычно слишком примитивен и не может похвастаться богатыми возможностями. Во всяком случае, дизассемблер, встроенный в WinDbg, недалеко ушел от DUMPBIN, с недостатками которого мы уже сталкивались. Насколько же понятнее становится код, если его загрузить в IDA!

Чем же тогда ценен отладчик? Дело в том, что дизассемблер из-за своей статичности имеет ряд ограничений. Во-первых, исследователю приходится выполнять программу на «эмуляторе» процессора, «зашитом» в его собственной голове, следовательно, необходимо знать и назначение всех команд процессора, и всех структур операционной системы (включая недокументированные).

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

В-третьих, дизассемблирование вынуждает реконструировать алгоритм каждой функции, в то время как отладка позволяет рассматривать ее как «черный ящик» со входом и выходом. Допустим, имеется у нас функция, которая расшифровывает основной модуль программы. В дизассемблере нам придется сначала разобраться в алгоритме шифрования (что может оказаться совсем не просто), затем переложить эту функцию на язык IDA-С, отладить ее, запустить расшифровщик... В отладчике же можно поручить выполнение этой функции процессору, не вникая в то, как она работает, и, дождавшись ее завершения, продолжить анализ уже расшифрованного модуля программы.

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

При этом возникает естественное желание видеть в отладчике все те символьные имена, которые были внесены в дизассемблерный листинг. И IDA Pro предоставляет два способа это сделать! Рассмотрим оба.

 

Способ 1

Вернемся в Windows 10 и загрузим в IDA Pro файл first.exe (или ранее созданный «Идой» проект). Выберем в меню File подменю Produce file, а в нем — пункт Create MAP file. На экране появится окно с запросом имени файла (введем, например, first.map), а затем откроется модальный диалог, уточняющий, какие имена стоит включать в map-файл. Нажмем Enter, чтобы оставить все галочки в состоянии по умолчанию.

Мгновение спустя на диске образуется файл first.map, содержащий всю необходимую отладочную информацию в map-формате Borland. Отладчик WinDbg не поддерживает такой формат, поэтому перед его использованием файл необходимо конвертировать в формат DBG — отладочный формат Microsoft.

Конвертировать можно с помощью утилиты map2dbg, свободно распространяемой вместе с исходными кодами. Запускать ее нужно из командной строки. В один каталог с ней кладем map-файл и соответствующий .exe. Затем в нашем случае выполняем команду map2dbg first.exe.

В результате утилита выведет число преобразованных символов, а в текущей папке будет создан новый файл с расширением dbg. Теперь можно загрузить first.exe в WinDbg. При этом, если first.dbg находится в том же каталоге, файл подхватится автоматически и будет скопирован в системную папку C:\ProgramData\dbg\sym\first.dbg\ для дальнейшего исследования экзешника.

Сейчас в WinDbg надо выполнить команду .reload /f. Она заставит отладчик перезагрузить информацию из модулей. Затем можешь выполнить lm, чтобы увидеть список загруженных модулей. Модуль first будет отмечен как codeview symbols, иначе было бы deferred:

00400000 0041d000 first C (codeview symbols) C:\ProgramData\Dbg\sym\first.dbg\5D5D59DE1d000\first.dbg

Исполнение команды x first!* выведет все символы в файле first.exe (показана только малая часть списка):

...

004028c4 first!std::_String_const_iterator<char,std::char_traits,std::allocator >::_String_const_iterator<char,std::char_traits,std::allocator > =

00402913 first!std::basic_streambuf<char,std::char_traits >::_Xsgetn_s =

0040298e first!std::basic_streambuf<char,std::char_traits >::xsputn =

00402a57 first!std::basic_filebuf<char,std::char_traits >::_Init =

00402a9e first!std::basic_string<char,std::char_traits,std::allocator >::basic_string<char,std::char_traits,std::allocator > =

00402adb first!std::_Fgetc =

00402af6 first!std::_Fputc =

00402b12 first!std::_Ungetc =

00402b30 first!std::basic_filebuf<char,std::char_traits >::sync =

00402b5b first!std::basic_filebuf<char,std::char_traits >::pbackfail =

00402bc5 first!std::basic_filebuf<char,std::char_traits >::underflow =

00402c29 first!std::basic_filebuf<char,std::char_traits >::setbuf =

00402c70 first!std::basic_string<char,std::char_traits,std::allocator >::begin =

00402c90 first!std::_Locinfo::~_Locinfo =

...

Посмотрим содержимое системной переменной:

0:000> da first!aSouthAfrica

00415500 "south-africa"

Способ 2

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

Теперь можно подключить WinDbg к IDA. Для этого надо открыть конфигурационный файл ida.cfg, находящийся в каталоге С:\Program Files\IDA 7.0\cfg\. Проматываем его содержимое до такой строки:

//DBGTOOLS = "C:\\Program Files\\Debugging Tools for Windows (x86)\\";

Ниже или вместо нее вставить путь к инструментам отладки WinDbg. В моем случае:

DBGTOOLS = "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\";

Отсюда вытекает третья мудрость: в случае с IDA 7.0 и новее, даже если приложение дизассемблируется и отлаживается в 32-битной разновидности IDA, путь надо указывать к 64-битной WinDbg. Иначе при запуске отладки тебя ждет сообщение об ошибке.

Следующим шагом надо запустить IDA, выбрать WinDbg из ниспадающего списка на панели инструментов и, нажав F9, запустить отладку. Только отладка приложения first.exe закончится, не успев начаться. Нужно сделать так, чтобы на точке входа программа замерла. Для этого надо вызвать диалоговое окно Debugger setup (пункт Debugger options в меню Debugger).

Debugger_setup.png.a0354c09a88c974f4d19e65b2d1f0cfe.png

Нашу задачу призвана решить область Events. Здесь можно выбрать, на каких событиях мы хотим подвешивать программу. Поставим третий флажок Suspend on process entry point, в результате чего выполнение проги приостановится на стартовом коде.

Program_suspend.thumb.png.831dddf514489a82f0b08624a625b8b1.png

Знакомые места? Еще бы! Обрати внимание: программа оперирует регистрами процессорной архитектуры x86-64: RCX, RDX, RCI и так далее. Ядро Windows 10 экспортирует 1595 символов, учитывая все установленные обновления операционной системы на моем компьютере.

Это можно проверить, дважды щелкнув на модуле kernel32.dll в окне Modules во время отладки проги «Идой». Откроется дополнительная вкладка Module: KERNEL32.DLL. Ее можно отцепить и перетащить в любое место. На нижней части рамки окна отображается общее количество символов, экспортируемых данным модулем.

Подключение WinDbg к IDA позволяет «Иде» использовать символьную информацию модулей с публичного сервера Microsoft. Для этого можно создать переменную окружения или определить директорию непосредственно из IDA без ее перезапуска. Пойдем второй, более короткой дорогой, а создать переменную окружения ты можешь на досуге. В командную строку IDA (в нижней части окна рядом с кнопкой WINDBG) введи:

.sympath srv*c:\debugSymbols*http://msdl.microsoft.com/download/symbols

После этого перезагрузи символы командой .reload /f. Число экспортируемых модулем kernel32.dll символов стало 5568.

Теперь символьные имена не только отображаются на экране, что упрощает понимание кода, — на любое из них можно быстро и с комфортом установить точку останова (скажем, bp GetProcAddress), и отладчик поймет, что от него хотят! Нет больше нужды держать в памяти эти трудно запоминаемые шестнадцатеричные адреса!

 

Заключение

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

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

×
×
  • Create New...