C++ C++ C# C# ASP.NET Security ASP.NET Security ASM ASM Скачать Скачать Поиск Поиск Хостинг Хостинг  
  Программа для работы с LPT портом...
Язык: .NET — ©Alexey...
  "ASP.NET Atlas" – AJAX в исполнении Micro...
Язык: .NET — ©legigor@mail.ru...
  "Невытесняющая" Многопоточность...
Язык: C/C++ — ©...
  01.05.2010 — Update World C++: Сборник GPL QT исходников
  15.12.2007 — Весь сайт целиком можно загрузить по ссылкам из раздела Скачать
Хостинг:
Windows 2003, ASP.NET 2.0
бесплатный и от 80 руб./мес


   Отправить письмо
Кулабухов Артем, Беларусь




 Реверсивное программирование / Reverse / Ассемблер в C/C++

.. Реверсивное программирование  //dev0id

Задача реверсивного программирования: получение исходного кода исследуемой программы. Данная статья рассматривает архитектуру процессора 8086 и выше. Для понимания материала необходимы знания ассемблера.

На нашем сайте (rootteam.void.ru) выложен релиз дизассемблера, я же собираюсь вам описать базовые принципы его работы для понимания того, как он устроен, и как программа получает исходный код.

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

- поле префиксов
- поле кода операции
- байт modrm
- байт sib
- смещение в команде
- непосредственный операнд

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

Поле префиксов
Сами префиксы делятся на 5 групп:

- префиксы повторения
          rep(e)(z)     0f3h
          repn(e)(z)   0f2h
- префикс блокировки шины
          lock             0f0h
- префиксы замены сегмента
          cs                2eh
          ds                3eh
          es                26h
          ss                36h
          fs                 64h
          gs                65h
- префикс замены размера операнда
          66h
- префикс замены размера адреса
          67h

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

36 2e 26 36 90

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

Несколько слов о последних двух группах префиксов 66h и 67h
Используются они для изменения размера операнда и адреса. Дело в том, что по умолчанию используется 16-разрядная адресация, из чего следует, что и регистры, и адреса в пямять используются 16-разрядные. После того, как будут использованы данные префиксы, размеры изменяться на 32-разрядные. Вот пример команды SUB CX, DX:

2B CA

А вот пример команды SUB ECX, EDX:

66 2B CA

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

КОП
Код операции имеет длинну либо один, либо два байта. На сайте developer.intel.com есть три тома полной документации для программистов на асме. Посмотрите, в аппендиксе второго тома лежат таблицы кодировки однобайтного и двухбайтного копов. Основные комадны находятся в первой - однобайтной таблице. Сама таблица выглядит следующим образом: размер таблицы 16х16 в строках

0-3      матиматические операции+логика
4         inc dec
5         push pop
6         тяжелая арифметика и управляющие команды
7         условный переход
8         легкая арифметика/логика в специальном формате, команды пересылки в спец.формате
9         xchg регистр, регистр+еще некоторые команды
A         строковые команды+команды управления
B         mov в спец формате с непосредственными значениями
C         команды управления
D         команды сопроцессора
E-F      много всего остального

Это касательно однобайтного КОПа. Двухбайтный я расписывать не буду, скажу лишь, что его первый байт всегда начинается с 0fh, что позволяет быстро его определить. Теперь несколько слов о том, что нам может сказать КОП.
В КОПе также встречаются некоторые поля, но это уже спецификация команд, так как не во всех КОПах, к примеру, кодируется регистр. Вот какие битные поля бывают в КОПе:

w - определяет, будет ли использоваться 8-разрядная адресация. Этот бит в КОПе идет почти всегда последним и используется почти во всех командах.
d - определяет, в каком направлении идет пересылка данных. Подробнее мы о нем поговорим при разборе modrm.
s - отвечает за расширение операнда до 16- или 32-разрядного. Поле reg уже упоминало о том, что в некоторых КОПах кодируется регистр. Используется вместе с битом w.

Несколько слов о том, как кодируются регистры:

	     разрядность
reg:	8	16	32
000	al	ax	eax
001	cl	cx	ecx
010	dl	dx	edx
011	bl	bx	ebx
100	ah	sp	esp
101	ch	bp	ebp
110	dh	si	esi
111	bh	di	edi
modrm
Сразу хочу привести несколько таблиц, без которых будет невозможно разобрать байт modrm. Данную таблицу можно найти в той же документации "Intel" или в "Специальном справочнике" В.Юрова.
16-разрядная адресация                      32-разрядная адресация
               (см. "Специальный справочник" В.Юров)

адрес			mod	rm		адрес
[bx+si]			00	000		[eax]
[bx+di]			00	001		[ecx]
[bp+si]			00	010		[edx]
[bp+si]			00	011		[ebx]
[si]			00	100		[sib]
[di]			00	101		offset32
offset16		         00	110		[esi]
[bx]			00	111		[edi]
[bx+si]+offset8		01	000		offset8[eax]
[bx+di]+offset8		01	001		offset8[ecx]
[bp+si]+offset8		01	010		offset8[edx]
[bp+si]+offset8		01	011		offset8[ebx]
[si]+offset8		01	100		offset8[sib]
[di]+offset8		01	101		offset8[ebp]
[bp]+offset8		01	110		offset8[esi]
[bx]+offset8		01	111		offset8[edi]
[bx+si]+offset16        	10	000		offset32[eax]	
[bx+di]+offset16	         10	001		offset32[ecx]
[bp+si]+offset16	         10	010		offset32[edx]
[bp+si]+offset16	         10	011		offset32[ebx]
[si]+offset16		10	100		offset32[sib]
[di]+offset16		10	101		offset32[ebp]
[bp]+offset16		10	110		offset32[esi]
[bx]+offset16		10	111		offset32[edi]
eax/ax/al		         11	000		eax/ax/al
ecx/cx/cl		         11	001		ecx/cx/cl
edx/dx/dl		         11	010		edx/dx/dl
ebx/bx/bl		         11	011		ebx/bx/bl
esp/sp/ah		         11	100		esp/sp/ah
ebp/bp/ch		         11	101		ebp/bp/ch
esi/si/dh		         11	110		esi/si/dh
edi/di/bh		         11	111		edi/di/dh
Как видите, восьмиразрядное смещение может быть использовано как в 16-, так и в 32-разрядной сетке. Сам байт modrm кодируется следующим образом:

mod reg rm

Из приведенных выше таблиц видно, что под mod отводится 2 бита, под reg - 3 и под rm, также 3. Пример. Рассмотрим команду sub cx, dx. Из скришотов -CB CA алгоритм следующий:

    1. Проверяем префиксы (с учетом "защиты" от дизассемблирования).
    2. Находим КОП.
    3. В зависимости от КОПа решаем, считывать ли нам следующий байт или нет, и решаем, что это у нас будет             modrm? offset? imm(непосредственный операнд)?

Здесь у нас нет префиксов, следовательно у нас адресация 16-разрядная. По таблице опкодов intel'а ищем CBh. Проверяем бит w:

      C         B
1 1 0 0   1 0 1 1
                      w

Так, значит w равен 1, следовательно используется полная адесация, т.е. та, которая определена префиксами, а так как их нет, значит 16-разрядная. Это SUB, следовательно дальше точно идет modrm - CA. Разбиваем его:

      С         А
1 1 0 0   1 0 1 0

Значит mod у нас равен 3, reg равен 1, а rm - 2. Смотрим по таблице регистров, что за регистр под номером 1 - это CX, а по таблице modrm'а видно, что при mod = 3, операдном выступает не область памяти, а регистр под номером rm - dx (16-разрядная адресация). Получаем окончательный ответ:

sub     cx, dx.

Рассмотрим другой пример. Пусть на этот раз у нас будет вычитаться из регистра CX ячейка памяти по адресу "[bp+si]+1234h", и пусть при этом будет использоваться префикс замены сегмента (с "защитой" от дизасма =)). Выглядит это послу ручной сборки так: 2E 3E 2B 8A 34 12. Начнем разбирать:

2eh      префикс замены сегмента на CS
3eh      префикс замены сегмента на DS

Значит процессор поймет только последний из группы, а это - 3eh. Отлично, продолжим. По таблице intel'a, 2B - КОП операции sub, регистр, память. Проверим бит w:

      2         B
0 0 1 0   1 0 1 1
                      w

Так, w равен 1, следовательно, используется полная адресация, то есть та, которая определена префиксами, а так как их нет, значит 16-разрядная. Считываем modrm:

    8A
10 001 010 
mod   равен 2
reg   равен 1
rm    равен 2
По таблице modrm'a при mod=2 и r=2 адрес [bp+si]+offset16, следовательно после modrm идет еще два байта смещения (в памяти они записываются со старших разрядов) 34 12. Окончательный ответ:

sub     cx, word ptr ds:[bp+si]+1234h

Это то, что касается 16-разрядной адресации. А что же при 32х? Вы наверное заметили, что в адресе упоминается sib. Это еще один байт расширения, в котором записывается масштабный множитель. Если встретился байт sib, то понадобиться еще одна таблица:

		таблица sib
масштаб	          множитель       индекс
		   (ss)	        index
[eax]				000
[ecx]				001
[edx]				010
[ebx]				011
-----				100
[ebp]				101
[esi]				110
[edi]				111
Сам sib-байт кодируется следующим образом:

ss index base (размер полей такой же как и в modrm-байте)

base играет роль reg'а из modrm, только он всегда 32-разрядный. Множитель ss показывает, на сколько нужно умножить масштаб, вычисляемый index'ом. Затем, умноженный масштаб складывается с base'ом. Стоит заметить, что при ss равном нулю, умножения не происходит, а при равенстве base 5, считается, что base отсутствует, и сложения не происходит. Пример:

Будем вычитать из eax ячейку памяти по адресу [eax*2]

66 67 2B 04 45 00 00 00 00

Тааак... это будет последним и самым сложным примером. Проверяем префиксы: 66h, 67h. Разрядность регистра и памяти - 32. КОП 2B (см. предидущий пример). Далее проверяем modrm:

     04h
00 000 100

Так как разрядность 32(67h), смотрим по таблице modrm для 32-разрядной адресации. reg = 0, следовательно eax (так как префикс 66h). rm = 4, будем проверять sib-байт:

     45h
01 000 101

ss = 1 - множитель 2.
base = 5 - считаем, что нету базы.
index = 0 - по таблице sib'a [eax*2]

Отлично. Ответ:

sub     eax,dword ptr[eax*2]

Число появляется практически всегда, если base равен 5 (т.е. ebp), ну а так как число равно 0, им можно пренебречь, хотя можете его записать

sub     eax,dword ptr[eax*2]+00000000h

Конечно, это только базовые знания, необходимые для написания собственного дизассемблера, но без них будет очень сложно, поэтому, я надеюсь, что вам пригодиться эта статья. И не забудьте полистать доки на developer.intel.com.