Техника и философия хакерских атак
Продолжение...
Возможно, кому-то эта схема покажется витиеватой и трудной для запоминания, но зубрить все режимы без малейшего понятия о механизме их взаимодействия еще труднее; кроме того, нет возможности себя проверить и проконтролировать ошибки.
Действительно, в поле R/M все три бита тесно взаимосвязаны, в отличие от поля mod. Оно фактически задает длину следующего элемента в байтах.
Например:
[Reg+Reg] ---------¬ -----T-----T-----¬ ¦ опкод ¦ ¦ 00 ¦ reg ¦ Mem ¦ L--------- L----+-----+------ 7 0 7 0
[Reg+Reg+Offset8] ---------¬ -----T-----T-----¬ ------------¬ ¦ опкод ¦ ¦ 01 ¦ reg ¦ Mem ¦ ¦ offset 8 ¦ L--------- L----+-----+------ L------------ 7 0 7 0 7 0
[Reg+Reg+Offset16] ---------¬ -----T-----T-----¬ ------------¬ ¦ опкод ¦ ¦ 10 ¦ reg ¦ Mem ¦ ¦ offset 16 ¦ L--------- L----+-----+------ L------------ 7 0 7 0 15 0
Разумеется, не может быть смещения 'offset 14', (т.к. процессор не оперирует с полуторными словами) и комбинация '11' указывает на регистровую адресацию.
Может возникнуть вопрос: как складывать с 16-битным регистром 8 битное смещение? Разумеется, непосредственному сложению мешает несовместимость типов, поэтому процессор сначала расширяет 8 бит до слова с учетом знака. Таким образом, диапазон возможных значений младшего байта от -127 до 127 (или от -0x7F до 0x7F).
Все вышесказанное проиллюстрировано в таблице, расположенной ниже. Обратим внимание на любопытный момент: адресация [BP] отсутствует. Ближайшим эквивалентом этого является [BP+0]. Отсюда следует, что для экономии следует избегать непосредственного использования BP в качестве индексного регистра. BP может быть только базой. И хотя mov ax,[bp] и воспринимается любым ассемблером, но ассемблируется в mov ax,[bp+0], что на байт длиннее.
Исследовав приведенную ниже таблицу, можно прийти к выводу, что все виды адресации 8086 процессора были несколько неудобны. Сильнее всего сказывалось ограничение, позволяющее использовать в качестве индекса только три регистра (BX,SI,DI). Между тем гораздо чаще требовалось использовать для этого CX (например, в цикле) и AX (как возвратное значение функции).
Поэтому начиная с процессора 80386 (для 32-режима) концепция адресаций была пересмотрена. Поле R/M стало всегда выражать регистр, независимо от способа его использования. Последним же управляло поле 'mod', задающее (кроме регистровой) три вида адресации:
COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)
ZGT, ZGT
mod, Адрес
TTL9, TTL9
00,[Reg]
01, [Reg+08]
10, [Reg+32]
11, Reg
Видно, что поле mod по-прежнему выражает длину следующего поля - смещения, разве что с учетом 32-режима, где все слова расширяются до 32 бит.
Напомним, что с помощью префикса 0x67 можно и в 16-режиме использовать 32-режимы адресации, и наоборот. Однако при этом мы сталкиваемся с интересным моментом. Разрядность индексных регистров остается 32-битной и в 16-режиме!
В реальном режиме, где нет понятия границ сегментов, это действительно будет работать так, как выглядит, и мы сможем адресовать первые 4 гигабайта памяти (32 бита), что позволит преодолеть печально известное 64-килобайтовое ограничение 8086 процессоров. Но такие приложения окажутся нежизнеспособными в защищенном или V86 режиме. Попытка вылезти за границу 64-килобайтового сегмента вызовет исключение 0xD, что приведет к автоматическому закрытию приложения, - скажем, под управлением Windows. Аналогично поступают и отладчики (в том числе и многие эмуляторы, включая cup386).
Сегодня актуальность этого приема, конечно, значительно снизилась, поскольку "голый DOS" практически уже не встречается, а режим его эмуляции Windows'ом крайне неудобен для пользователей.
16 - режим 32 - режим
COLUMNS(6), DIMENSION(IN), COLWIDTHS(.8350,.9608,1.0208,.8633,.7808,.8342), WIDTH(5.0050), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)
ZGT, ZGT, ZGT, ZGT, ZGT, ZGT
Адрес, Mod, R/M, Адрес, Mod, R/M
TTL9, TTL9, TTL9, TTL9, TTL9, TTL9
[BX+SI], 00, 000, [EAX], 00 000
[BX+DI], 00, 001, [ECX], 00 001
[BP+SI], 00, 010, [EDX], 00 010
[BP+DI], 00, 011, [EBX], 00 011
[SI], 00, 100, [--][--], 00 100
[DI], 00, 101, смещ32, 00 101
смещ16 ^1, 00, 110, [ESI], 00 110
[BX], 00, 111, [EDI], 00 111
[BX+SI]+смещ8, 01, 000, смещ8[EAX], 01 000
[BX+DI]+смещ8, 01, 001, смещ8[ECX], 01 001
[BP+SI]+смещ8, 01, 010, смещ8[EDX], 01 010
[BP+DI]+смещ8, 01, 011, смещ8[EBX], 01 011
[SI]+смещ8, 01, 100, смещ8[--][--], 01 100
[DI]+смещ8, 01, 101, смещ8[ebp], 01 101
[BP]+смещ8, 01, 110, смещ8[ESI], 01 110
[BX]+смещ8, 01, 111, смещ8[EDI], 01 111
[BX+SI]+смещ16, 10, 000, смещ32[EAX], 10 000
[BX+DI]+смещ16, 10, 001, смещ32[ECX], 10 001
[BP+SI]+смещ16, 10, 010, смещ32[EDX], 10 010
[BP+DI]+смещ16, 10, 011, смещ32[EBX], 10 011
[SI]+смещ16, 10, 100, смещ32[--][--], 10 100
[DI]+смещ16, 10, 101, смещ8[ebp], 10 101
[BP]+смещ16, 10, 110, смещ8[ESI], 10 110
[BX]+смещ16, 10, 111, смещ8[EDI], 10 111
Изучив эту таблицу, можно решить, что система адресации 32-режима крайне скудная и ни на что серьезное ее не хватит. Однако это не так. В 386+ появился новый байт SIB (Scale-Index Base).
Процессор будет ждать его вслед за R/M взякий раз, когда последний равен 100b. Эти поля отмечены в таблице как '[--]'. SIB хорошо задокументирован, и назначения его полей показаны на рисунке ниже. Нет совершенно никакой нужды зазубривать таблицу адресаций.
-----------T------------T-----------¬ ¦ Scale ¦ Index ¦ Base ¦ L----------+------------+------------ 7 6 3 0
'Base' - это базовый регистр, Index - индексный, а два байта Scale - это степень двойки для масштабирования. Поясним введенные термины. Что такое индексный регистр, понятно всем. Например [SI]. Теперь же можно выбирать любой регистр в качестве индексного. Правда, за ислючением SP (впрочем, можно выбирать и его, но об этом позже).
Базовый регистр - это тот, который суммировался с индексным, например, [BP+SI]. Точно так же теперь можно выбрать любой регистр в качестве базового. При этом есть возможность выбрать SP. Заметим, что если мы выберем последний в качестве индексного, то получим вместо 'SP' - "никакой". В этом случае адресацией будет управлять только базовый регистр.
Наконец, масштабирование - это уникальная возможность умножать индексный регистр на 1,2,4,8 (т.е. степень двойки, которая задается в поле Scale). Это очень удобно для доступа к различным структурам данных. При этом индексный регистр, являющийся одновременно и счетчиком цикла, будет указывать на следующий элемент структуры даже при единичном шаге цикла, что чаще всего и встречается.
COLUMNS(11), DIMENSION(IN), COLWIDTHS(.8350,.9608,1.0208,.8633,.7808,.8342,.8342,.8342,.8342,.8342,.8342), WIDTH(5.0050), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)
TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9, TTL9
Base, , , EAX, ECX, EDX, EBX, ESP, , ESI, EDI
, , , 000, 001, 010, 011, 100, [*], 110, 111
Index, , S, Шестнадцатиричные значения SIB, +, +, +, +, +, +, +
[EAX], 000, 00, 00, 08, 10, 18, 20, 28, 30, 38
[ECX], 001, , 01, 09, 11, 19, 21, 29, 31, 39
[EDX], 010, , 02, 0A, 12, 1A, 22, 2A, 32, 3A
[EBX], 011, , 03, 0B, 13, 1B, 23, 2B, 33, 3B
Отсутствует, 100, , 04, 0C, 14, 1C, 24, 2C, 34, 3C
[EBP], 101, , 05, 0D, 15, 1D, 25, 2D, 35, 3D
[ESI], 110, , 06, 0E, 16, 1E, 26, 2E, 36, 3E
[EDI], 111, , 07, 0F, 17, 1F, 27, 2F, 37, 3F
[EAX*2], 000, , 40, 48, 50, 58, 60, 68, 70, 78
[ECX*2], 001, , 41, 49, 51, 59, 61, 69, 71, 79
[EDX*2], 010, , 42, 4A, 52, 5A, 62, 6A, 72, 7A
[EBX*2], 011, , 43, 4B, 53, 5B, 63, 6B, 73, 7B
Отсутствует, 100, , 01, 44, 4C, 54, 5C, 64, 6C, 74, 7C
[EBP*2], 101, , 45, 4D, 55, 5D, 65, 6D, 75, 7D
[ESI*2], 110, , 46, 4E, 56, 5E, 66, 6E, 76, 7E
[EDI*2], 111, , 47, 4F, 57, 5F, 67, 6F, 77, 7F
[EAX*4], 000, , 80, 88, 90, 98, A0, A8, B0, B8
[ECX*4], 001, , 81, 89, 91, 99, A1, A9, B1, B9
[EDX*4], 010, , 82, 8A, 92, 9A, A2, AA, B2, BA
[EBX*4], 011, , 83, 8B, 93, 9B, A3, AB, B3, BB
Отсутствует, 100, 10, 84, 8C, 94, 9C, A4, AC, B4, BC
[EBP*4], 101, , 85, 8D, 95, 9D, A5, AD, B5, BD
[ESI*4], 110, , 86, 8E, 96, 9E, A6, AE, B6, BE
[EDI*4], 111, , 87, 8F, 97, 9F, A7, AF, 77, BF
[EAX*8], 000, , C0, C8, D0, D8, E0, E8, F0, F8
[ECX*8], 001, , C1, C9, D1, D9, E1, E9, F1, F9
[EDX*8], 010, , C2, CA, D2, DA, E2, EA, F2, FA
[EBX*8], 011, , C3, CB, D3, DB, E3, EB, F3, FB
Отсутствует, 100, 11, C4, CC, D4, DC, E4, EC, F4, FC
[EBP*8], 101, , C5, CD, D5, DD, E5, ED, F5, FD
[ESI*8], 110, , C6, CE, D6, DE, E6, EE, F6, FE
[EDI*8], 111, , C7, CF, D7, DF, E7, EF, F7, FF
Если при этом в качестве базового индекса будет выбран BP, то полученный режим адресации будет зависеть от поля MOD предыдушего байта. Возможны следующие варианты:
COLUMNS(2), DIMENSION(IN), COLWIDTHS(1.2042,4.0883), WIDTH(8.1767), ABOVE(.0984), BELOW(.0984), HGUTTER(.0555), VGUTTER(.0433), BOX(Z_DOUBLE), HGRID(Z_SINGLE), VGRID(Z_SINGLE), KEEP(OFF), ALIGN(CT)
ZGT, ZGT
mod, Действие
TTL9, TTL9
00, Смещение32[index]
01, Смещение8 [EBP] [index]
10, Смещение32[EBP] [index]
Итак, мы полностью разобрались с кодировкой команд. Осталось лишь вычислить непосредственно саму таблицу опкодов, и можно отправляться в длинный и тернистый путь написания собственного дизассемблера.
За это время, надеюсь, у вас разовьются достаточные навыки для ассемблирования\дизассемблирования в уме. Впрочем, есть множество эффективных приемов, позволяющих облегчить сей труд. Ниже я покажу некоторые из них. Попробуем без дизассемблера взломать crackme01.com. Для этого даже не обязательно помнить опкоды всех команд!
00000000:B4 09 BA 77 01 CD 21 FE C4 BA 56 01 CD 21 8A 0E
¦ +.¦w.=!.-¦V.=!К. 00000010:56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81 ¦ V.З.м.рт.-;.0$FБ 00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А. 00000030:C3 74 08 B4 09 BA BE 01 CD 21 C3 B0 94 29 9A 64 ¦ +t.+.¦-.=!+-Ф)Ъd 00000040:21 ED 01 E3 2D 2A 70 41 53 53 57 4F 52 44 00 6F ¦ !э.у-*pASSWORD.o 00000050:6B 01 20 2A 04 B0 20 00 00 00 00 00 00 00 00 00 ¦ k..*.-.......... 00000060:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ¦ ................ 00000070:00 00 00 00 00 00 00 43 72 61 63 6B 20 4D 65 20 ¦ .......Crack Me 00000080:20 30 78 30 20 3A 20 54 72 79 20 74 6F 20 66 6F ¦ 0x0 : Try to fo 00000090:75 6E 64 20 76 61 6C 69 64 20 70 61 73 73 77 6F ¦ und valid passwo 000000A0:72 64 20 28 63 29 20 4B 50 4E 43 0D 0A 54 79 70 ¦ rd (c) KPNC Typ 000000B0:65 20 70 61 73 73 77 6F 72 64 20 3A 20 24 0D 0A ¦ e password : $ 000000C0:50 61 73 73 77 6F 72 64 20 66 61 69 6C 2E 2E 2E ¦ Password fail... 000000D0:20 74 72 79 20 61 67 61 69 6E 0D 0A 24 ¦ try again$
Итак, для начала поищем, кто выводит текст 'Crack me..Type password'. В самом файле начало текста расположено со смещением 0x77. Следовательно, если учесть, что com файлы загружаются начиная со смещения 0x100, эффективное смещение равняется 0x100+0x77==0x177. Учитывая обратное расположение старших и младших байт ищем в файле последовательность 0x77 0x01.
00000000: B4 09 BA 77 01 CD 21 ^^^^^
Вот она! Но что представляет собой опкод 0xBA? Попробуем определить это по трем младшим битам. Они принадлежат регистру DL(DX). А 0xB4 0x9 - это mov AH,9. Теперь нетрудно догадаться, что оригинальный код выглядел следующим образом:
MOV AH,9 MOV DX,0x177
И это при том, что не требуется помнить опкод команды MOV! (Хотя это очень распространенная команда, и ее опкод запомнить все же не помешает).
Вызов 21-го прерывания 0xCD 0x21 легко отыскать, если запомнить его символьное представление '=!' в правом окне дампа. Как нетрудно видеть, следующий вызов int 21 лежит чуть правее по адресу 0xC. При этом DX указывает на 0x156. Это соответствует смещению 0x56 в файле. Наверняка эта функция читает пароль. Что ж, уже теплее. Остается выяснить, кто и как к нему обращается. Ждать придется недолго.
--- чтение строки ¦ 00000000: B4 09 BA 77 01 CD 21 FE C4 BA 56 01 CD 21 8A 0E <-- 00001110
00000010: 56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81 ^ ^ ^^ ^
^ ^ ¦ LT-LT- ¦ ¦ смещение16 <------- ¦ ¦ L-T-- CL(CX)<-- L-> BP L-------- смещение пароля
При разборе байта 0xE не забудьте, что адресации [BP] не существует в природе. Вместо этого мы получим [offset16]. На размер регистра и приемник результата указывают два младших бита байта 0x8A. Они равны 10b. Следовательно, мы имеем дело с регистром CL, в который записывается содержимое ячейки [0x156].
Все, кто знаком с ассемблером, усмотрят в этом действии загрузку длины пароля (первый байт строки) в счетчик. Неплохо для начала? Мы уже дизассемблировали часть файла и при этом нам не потребовалось знание ни одного опкода операции, за исключением, быть может 0xCD == INT. Продолжим в том же духе.
Вряд ли мы скажем, о чем говорит опкод 0x87. (Впрочем, обращая внимание на его близость к операции NOP = xchg ax,ax, можно догадаться, что 0x87 - это опкод операции xchg). Обратим внимание на связанный с ним байт 0xF2:
F2 == 11110010
^^^ ^^ ^ ¦¦¦ ¦¦ ¦ Reg/Reg-+-LT-L-+--> (DX)
(SI)
Как нетрудно догадаться, эта команда заносит в SI смещение пароля, содержащееся в DX. Этот вывод мы делаем только исходя из смыслового значения регистров, полностью игнорируя опкод команды. К сожалению, этого нельзя сказать о следующем байте - 0xAC. Это опкод операции LODSB, и его просто придется запомнить.
0x02 - это опкод ADD, а следующий за ним байт - это AH,AL (не буду больше повторяться).
0xE2 это опкод операции LOOP, а следующий за ним байт - это знаковое относительное смещение перехода.
00000010: 56 01 87 F2 AC 02 E0 E2 FB BE 3B 01 30 24 46 81
^ ¦ L------5--------
Чтобы превратить его в знаковое целое, необходимо дополнить его до нуля (операция NEG, которую большинство калькуляторов не поддерживают). Тот же результат мы получим, если отнимем от 0x100 указанное значение (если разговор идет о байте). В нашем примере оно равно пяти. Отсчитаем пять байт влево от начала СЛЕДУЮЩЕЙ КОМАНДЫ. Если все сделать правильно, то вычисленный переход должен указывать на байт 0xAC == LODSB; впрочем, последнее было ясно и без вычислений, ибо других вариантов, по-видимому, не существует.
Почему? Да просто данная процедура подсчета контрольной суммы (или точнее, хеш-суммы) очень типична. Конечно, не стоит всегда полагаться на свою интуицию и "угадывать" код, хотя это все же сильно ускоряет анализ.
С другой стороны, хакер без интуиции - это не хакер. Давайте применим нашу интуицию и "вычислим", что представляет опкод следующей команды. Вспомним, что 0xB4 (== 10110100) это MOV AH,imm8
0xBE очень близко к этому значению, следовательно, это операция MOV. Осталось определить регистр-приемник. Рассмотрим обе команды в двоичном виде:
MOV AH, imm8 MOV ??, ???
* * 10110100 10111110 ^ ^^^ ^ ^ ^^^ ^ ¦ ¦¦¦ ¦ ¦ ¦¦¦ ¦ 'mov'<-+---¦L-+->AH (SP) 'mov'<+---¦L-+--> DH (SI) ¦ ¦ L--------> size <---------
Как уже говорилось выше, младшие три бита - это код регистра. Однако его невозможно однозначно определить без уточнения размера операнда. Обратим внимание на третий (считая от нуля) бит. Он равен нулю для AH и единице в нашем случае. Рискнем предположить, что это и есть бит размера операнда: хотя этого явно и не утверждается Intel, но вытекает из самой архитектуры команд и устройства декодера микропроцессора.
Заметим, что это, строго говоря, частный случай, - могло бы оказаться и иначе. Так, например, четверый справа бит по аналогии должен быть флагом направления или знакового расширения, но, увы, - таковым в данном случае не является. Четыре левые бита - это код операции 'mov reg,imm'. Запомнить его легко - это "13" в восьмеричном представлении.
Итак, 0xBE 0x3B 0x01 - это MOV SI,0x13B. Скорее всего 0x13B - это смещение, и за этой командой последует расшифровщик очередного фрагмента кода. А может быть и нет - это действительно смелое предположение. Однако 0x30 0x24 это подтверждают. Хакеры обычно настолько часто сталкиваются с функций xor, что чисто механически запоминают ее опкод.
Нетрудно установить, что эта последовательность дизассемблируется как XOR [SI],AH Следующий байт 0x46 уже нетрудно "угадать" - INC SI. Кстати, рассмотрим, что же интересного в этом опкоде:
_ 1000110 ^^ ^ ¦¦ ¦ ¦L-+-- AH\SI ¦
Третий бит равен нулю! Выходит, команда должна выглядеть как INC AH! (Что, кстати, выглядит непротиворечиво в смысле дешифовщика.) Однако все же это inc si. Почему мы решили, что третий бит - флаг размера? Ведь Intel этого никак не гарантировала! И INC byte вообще выражается через дополнительный код, что на байт длиннее.
Таким образом, как ни полезно знать архитектуру инструкций, все же таблицу опкодов хотя бы местами надо просто выучить. Иначе можно впасть в глубокие и грубые ошибки. Хотя, с другой стороны, знание архитектуры очень и очень помогает.
81 ¦ V.З.м.рт.-;.0$FБ 00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А.
Но продолжим наш анализ. 0x81 это, если можно так выразиться, "гейт". Наконец-то мы с ним столкнулись. Он открывает доступ к целому семейству команд, манипулирующих регистром-непосредственным значеним. "Гейтов" не так много, и всех их нужно знать наизусть. Они только указывают на группу команд, а конкретный код задается в поле reg следующего байта.
0x81: 10000001 -- 0xFE: 11111110 ^ ^^^ ^^ ^ ¦ ¦¦¦ ¦¦ ¦ size------- reg\imm<-+-¦ ¦L-+--> AH\SI LT- ¦
CMP
Таким образом, достаточно помнить, что 111b - обозначают CMP. Хотя об этом косвенно свидетельтсвует и сам операнд. Взгляните:
_____ 00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А. ^^^^^
Мы должны помнить, что это смещение нам уже встречалось в качестве буфера для вводимого пароля. Можно предположить, что расшифровщик проверяет границы и прекращает шифровку в этом месте. Ясно видно, что зашифрованный блок лежит между расшифровщиком и самим паролем. И тогда команда cmp SI, offset psw логична и уместна.
Кому-то описываемый подход может показаться ненаучным, а то и полумистическим. Но это не так. Надо просто почаще ставить себя на место разработчика и думать: "а как бы я написал этот код?". В большинстве случаев действие потенциального противника можно предугадать.
00000020:FE 56 01 72 F7 4E 02 0C 81 FE 3B 01 73 F7 80 F9
¦ .V.rўN..Б.;.s.А. ^^^^^
Команды из серии 7x известны, наверное, каждому, кто хотя бы раз "правил байтики" в чужой программе. Конечно же, это условный переход! А как узнать само условие? Для этого необходимо вспомнить, о чем я говорил выше, и рассмотреть эту команду более детально:
1110010 ^^^^ ^^ ¦ ¦¦ ¦¦ Jx<-+--LT-L--> флаг '!(условие)'. ¦ условие
Само условие (упакованный регистр флагов) есть проверка флага переноса или, в более понятной мнемонике, JB. Самый правый бит - логическое 'NOT' равен нулю; следовательно, это "прямое" условие, т.е. JB xxx так и есть. Адрес перехода 0xF7 можно угадать не вычисляя. Но на всякий случай проверим:
--->------9-------->-¬ 00000010:56 01 87 F2 AC^02 E0 E2 FB BE 3B 01L30 24 46 81 ¦ V.З.м.рт.-;.0$FБ
00000020:FE 56 01 72 F7
L4E 02 0C 81 FE 3B 01 73 F7 80 F9 ¦ .V.rўN..Б.;.s.А.
Оно равно -9 и переходит на операцию 'XOR'. Последующий код очень близок к 0x46 - INC SI. Особенно это хорошо заметно в двоичном представлении:
0x46 : 1000110
¦¦¦¦ норма <---L++----> SI инверс <--¬-TT----> SI ¦¦¦¦ 0x4E : 1001110
Тут мы сталкиваемся еще с одиним недокументированным полем, которое характерно для некоторых команд. А именно с полем знака. Во втором случае он отрицательный. Следовательно, INC сменяется на DEC. Конечно, не зная самой команды, нельзя предугадать значение третьего бита, но тут есть одна хитрость. Дело в том, что однотипные команды объединены в группы, в которых действуют свои локальные условия. Отбросив три бита, мы получим, что 0x46 и 0x4E отличаются друг от друга всего на единицу, а значит "территориально" очень близки.
То же можно сказать и про опкод 0x2. Наверно, большинство знает, что 0x0 0x0 0x0... это сложение чего-то с чем-то, не так ли? Теперь можно установить, что 02 или 10 в двоичном представлении - это сложение одного операнда размером в байт с другим. Что это за операнды - покажет следующее поле 0xC:
00001100 ^^^^^^^^ ¦¦¦ ¦¦¦¦ r,m8 <---+-¦ ¦¦¦¦ CL <-----+--¦¦¦ базирование <---------¦¦ индексный р <----------¦ регистр SI <-----------
Таким обазом, получается CL -> [SI]. Но, вспоминая обратный бит направления в предыдущем байте, меняем операнды местами. И у нас получается ADD CL,[SI]. Похоже, считается контрольная сумма расшифрованного фрагмента. Очевидно, что следующей командой будет CMP SI,0x13B.
-----<-------9------<------¬ 00000020:FE 56 01 72 F7L4E 02 0C 81 FE 3B 01 73 F7-80 F9 ¦ .V.rўN..Б.;.s.А. ^^ ^^^^^
Действительно, это смещение наблюдается в дампе, равно как и 0x81. Следовательно, 0xFE в таком случае будет cmp si,offset 16. И это вряд ли нужно проверять.
А теперь обратим внимание на следующий код (0x73 0xF7 по понятным причинам мы опускаем):
_____ 80 F9 ¦ .V.rўN..Б.;.s.А.
Нам он уже встречался. Нетрудно вспомнить, что это cmp cl,imm8. Само непосредственное значение можно найти в следующем байте:
00000030:C3 74 08 B4 09 BA BE 01 CD 21 C3 B0 94 29 9A 64
¦ +t.+.¦-.=!+-Ф)Ъd ^^
Оно равно 0xC3. Теперь следующая команда передает управление расшифрованному коду. Чтобы это действительно произошло, необходимо ввести правильный пароль. Как его найти, было подробно рассказано выше; кроме того, этот пример был разобран "по косточкам" и успешно взломан. Поэтому не будем на этом останавливаться.
Но, чтобы найти пароль нужно написать переборщик, а в нашем распоряжении, как мы условились, ни одного компилятора нет. И тут мы подходим к следующему этапу: как писать программы непосредстенно "с нуля", без единого инструмента под рукой...
Маленькие хитрости
Главная часть дисциплинирующей выучки- это ее сокрытая часть, предназначенная не освобождать, но ограничивать.
Ф. Херберт "Еретики Дюны".
Хорошо, если в вашем распоряжении окажутся шестнадцатиричный и двоичный калькуляторы. Но в некоторых ситуациях и они недоступны. Конечно, это преувеличение и если продолжать экстраполировать, то можно сказать, что иногда вообще калькулятора под рукой может не оказаться.
Но хакер должен расчитывать на самое худшее и привыкать полагаться только на самого себя. Тем более что ничего сложного в этих операциях нет. Как можно перевести произвольное число в двоичное? Для этого нужно поделить его на 2 и записать остаток в младший разряд. И так до тех пор, пока делить станет нечего. Или, другими словами, нам нужно вспомнить признак делимости на два. Все мы его проходили в школе. Если последняя цифра числа делится на два, то и все число делится на два. Хорошо, а как разделить, если нет калькулятора и даже счетов?
Разумеется, в столбик. При этом можно легко оперировать и шестнадцатиричными числами (при вычислении в столбик это не составляет существенного затруднения).
Однако этот способ несколько утомителен. Куда проще запомнить (или вычислить в уме) ряд квадратов:
1 2 4 8 16 32 64 128
Ясно, что любое число от нуля до 255 представляет собой их сумму. Причем каждая степень может встречаться только один раз. Покажем это на примере. Допустим, нам необходимо узнать двоичное представление числа 99. Начиинаем с конца. Число нечетное, значит, в сумме фигурирует единица, т.е. младший бит равен единице. Отнимаем от 99 один и получем 98. Если отнять еще и двойку, то получим 96, а 96 == 32 + 64 как легко можно видеть. Итого в двоичном виде это - 1100011. Конечно, это потребует определенных навыков устного счета, но все же достаточно просто, чтобы не обращаться каждый раз к калькулятору.
Аналогично можно любое число из двоичного перевести в десятичное. Например:
1001b == 1+2*0+4*0+8*1 == 1+8 == 9
Все вышесказанное не в меньшей мере применимо и к шестнадцатиричным числам:
0x1 0x2 0x4 0x8 0x10 0x20 0x40 0x80
Причем все математические операции с такими круглыми числами делать в уме на порядок проще, за что я и люблю шестнадцатиричную систему счисления.
Разумееся, описанные приемы ни для кого не дожны быть новостью, и все они входят в школьный курс информатики и вычислительной техники. Удивительно, но, покинув стены школы, многие о них забывают! Отчасти это оправдано, но все же бывают в жизни ситуации, когда человек остается наедине с машиной, без прикладного ПО и все эти операции волей-неволей приходится выполнять в уме.
Или более правдоподобная ситуация: вы в поезде (трамвае, автобусе) изучаете распечатку программы, а калькулятор, как назло, забыли взять. Как ни редко, но все же и такое случается!
Ассемблирование в уме
Ничто не превосходит по сложности человеческий ум.
Ф. Херберт "Еретики Дюны".
Мы уже проделали титаническую работу, дизассемблировав в уме крохотный файл в пару десятков байт. При этом трудозатраты выглядели весьма внушительно. Так реально ли использовать такой подход для анализа приложения хотя бы в пару килобайт? И сколько на это уйдет времени?
Другими словами, и нужно ли хоть кому-нибудь то, чем мы тут занимаемся? И если нужно, то при каких обстоятельствах? Нужно: во-первых тогда, когда иного выбора просто нет. С другой стороны, тренированный взгляд даже в километровом дампе (при беглом просмотре последнего) может заметить последовательности, характерные для защитного механизма... или для вируса.
Кстати, это типичный случай - когда необходимо удостовериться в наличии вируса в полученном файле. Достаточно дизассемблировать всего несколько десятков команд, чтобы все стало ясно - вирус это или нет. Конечно, не обязательно бывает именно так, но очень и очень часто. При этом посмотреть файл по "F3" гораздо быстрее, чем искать дизассемблер.
Но иногда на машине нет ни одного компилятора, а требуется написать хотя бы простенькую прогамму. Например, для удаления того же вируса (при условии, что обычные антивирусы его "не берут"). Если еще усложнить задачу, то можно представить, что в нашем распоряжении нет не только компилятора, но и даже шестнадцатиричного редактора.
Кажется, что в такой ситуации ничего сделать невозможно. И администраторы подобных систем уверены, что они на 100% защищены от злоумышленников. Однако это лишь распространенное заблуждение. В MS-DOS есть возможность создавать бинарные файлы с помощью клавиши Alt и вспомогательной цифровой клавитуры. Когда-то это входило практически во все руководства по IBM XT\AT, а сейчас уже никем и нигде не упоминается.
Давайте воскресим этот древний "обряд" и создадим маленький бинарный файл, который ничего не делает, а только возвращает управление MS-DOS. Для этого дадим команду:
copy con test.com
Она вызовет примитивнейщий текстовой редактор системы, но его возможностей для нас в данный момент будет предостаточно. Убедившись, что индикатор "Num Lock" горит, нажмем ALt и, не отпуская ее, на цифровой клавитуаре наберем 195. Отпустим Alt и нажмем Ctrl-Z для закрытия файла и выхода из редактора.
Запустим полученный файл. Он ничего не делает, но и не зависает. Дизассемблировав его, мы поймем, что он состоит всего из одной команды RETN (0xC3 == 195). Конечно, это довольно незатейливый пример, и реализацию можно улучшить, если ввести "магическую" последовательность, показанную ниже.
Alt-180 Alt-09 Alt-186 Alt-09 Alt-01 Alt-205 ! 195 Alt-32 Hello,Sailor!$ Ctrl-Z
Как нетрудно догадаться, мы получим com-файл, выводящий указанную фразу на экран. Действительно, он это и делает. Но обратите особое внимание на то, что мы его создали используя только штатные средсва MS-DOS, которые есть на любой машине, где есть MS-DOS (или Windows).
Точно так же можно написать и любую троянскую программу, обойти установленную защиту или, наконец, одуматься и сделать все же что-то полезное. Например, уничтожить вируса, восстановить разрушенный диск, или сделать что-то другое, в зависимости от ситуации.
Дизассемблируем только что полученный файл и обратим внимание на один ключевой момент:
seg000:0100 start proc near seg000:0100 mov ah, 9 seg000:0102 mov dx, offset aHelloSailor ; "Hello,Sailor!$" seg000:0105 int 21h ; DOS - PRINT STRING seg000:0107 retn seg000:0107 start endp seg000:0108 db 20h ; ^^^^^^^ seg000:0109 aHelloSailor db 'Hello,Sailor!$' ; DATA XREF: start+2o
Зачем в этом месте стоит незначащий символ? Не лучше ли было избавиться от него? Увы, это никак не получится:
00000000: B409 mov ah,009 ; 00000002: BA0901 mov dx,00109 ^^ 00000005: CD21 int 021
Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15