Форум АНТИЧАТ

Форум АНТИЧАТ (https://forum.antichat.xyz/index.php)
-   Защита ОС: вирусы, антивирусы, файрволы. (https://forum.antichat.xyz/forumdisplay.php?f=80)
-   -   CRC32 на fasm: почему таблица - это магия (https://forum.antichat.xyz/showthread.php?t=592534)

Marylin 30.03.2026 00:40

1. Общие сведения
2. Побитовый метод вычисления
3. CRC32 с использованием таблиц
4. Как получить на выходе нуль
5. Применение на практике
6. Послесловие

1. Общие сведения

CRC или "Cyclic Redundancy Check" (контроль циклически избыточным кодом) - это одна из разновидностей подсчёта контрольной суммы блока данных. Например, простейшей проверкой целостности информации можно считать использование бита чётности (или паритета, что одно и то-же). Это когда суммируются все биты передаваемого сообщения, и если сумма оказывается чётной, то в конец сообщения добавляется 0, если нечётной – то 1. При приёме повторно пересчитывается сумма битов, и сравнивается с принятым битом чётности. Если они отличаются, значит при передаче имеем потери.

Но такой способ определения ошибок оставляет желать лучшего, т.к. во-первых на каждый байт требуется лишний бит паритета(9), а во-вторых при искажении не одного, а нескольких битов сообщения, чётность суммы может не измениться. Поэтому существует множество более продвинутых проверок, в том числе CRC.

По сути CRC это не сумма, а остаток от деления некоего объёма информации на константу. Тем не менее, CRC исторически называют "контрольная сумма". Алгоритм считается потоковым (а не блочным), когда в итоговое значение CRC вносит вклад каждый бит. Если хоть один из них сломается, контрольная сумма тут-же слетит, причём существенно.

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

Это число в диапазоне 16/32/64-бита, которое назвали "Полином". В таблице ниже представлены общепринятые из них. Значения с зеркальным отражением бит используются, когда данные поступают от младшего бита к старшему "Little Endian". Для тех кто в танке, little endian - это способ хранения многобайтных чисел в памяти компьютера, при котором младший байт записывается по наименьшему адресу, а старший - по наибольшему. Это основной стандарт для компьютеров на базе х86. Например число
Код:

0x12345678
будет храниться в ОЗУ как:
Код:

78 56 34 12
. В отличие от него "Big-Endian" располагает байты в порядке убывания (как мы пишем на листе бумаги), и в комп.системах используется при передачи данных по сети. Таким образом, документированным полиномом для CRC-32 является значение
Код:

0xEDB88320
- именно его мы и будем использовать в своём коде.

Как видно из этой таблички, помимо полинома в расчётах принимает участие значение инициализации, а так-же значение XOR уже готового CRC на выходе. Значение Init - это CRC на старте, что позволяет вычислять контрольную сумму данных со-значением нуль (на случай, если данные начинаются с нуля). Соответственно на выходе из функции нужно будет внести эту поправку, для чего и предусмотрено значение "XOR out".

CRC-32 настолько популярен, что начиная с м/архитектуры "Nehalem" в набор SSE4.2 процессоров Intel была включена специальная инструкция
Код:

CRC32
, которая служит для аппаратного ускорения вычисления контрольной суммы. Она использует полином от
Код:

Castagnoli = 0x1EDC6F41
, и позволяет обрабатывать байты, слова или двойные слова в потоке данных, всего за один такт. Перед использованием инструкции необходим тест на поддержку через
Код:

cpuid eax,1
- если взведён бит(20) в регистре
Код:

ECX
, значит ок, иначе задействуем перечисленные ниже программные методы вычисления CRC-32.


2. Побитовый метод вычисленияCRC-32

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

1. Начинаем с 32-битной контрольной суммы 0xffffffff.
2. Сдвинуть предыдущее 32-битное значение CRC вправо на 8-бит.
3. XOR значения из пункта(2) с полиномом, чтобы получить новое значение CRC.
4. Повторить пункты(2,3) по всей длине данных.
5. После завершения цикла выполните XOR вычисленного значения CRC с 0xffffffff (можно NOT).
6. Это окончательный результат CRC32.

Вот реализация этого алгоритма, которая оформлена в процедуру:

C-подобный:


Код:

;
//==============================================
;
// Версия для малых объёмов данных (медленная)
;
// На входе: ESI = адрес данных, ECX = длина
;
// На выходе: EAX = CRC32
;
//==============================================
calc_crc32_slow
:
push    ebx ecx edx
 
    mov    eax
,
0xFFFFFFFF
;
// начальное значение CRC32
or      ecx
,
ecx
;
// есть что считать?
jz      @done_slow
;
// нет - на выход.
@calc_loop_slow
:
movzx  ebx
,
byte
[
esi
]
;
// загружаем очередной байт данных
xor    eax
,
ebx
;
// ксор с CRC
mov    edx
,
8
;
// обрабатываем все 8-бит байта
@bit_loop
:
shr    eax
,
1
;
// сдвиг вправо на 1
jnc    @no_xor
;
// пропустить, если выдвинулся нуль
xor    eax
,
0xEDB88320
;
// иначе: ксор с полиномом
@no_xor
:
dec    edx
;
// счётчик бит -1
jnz    @bit_loop
;
// повторить для всех 8-бит
inc    esi
;
// следующий байт данных
dec    ecx
;
// длина -1
jnz    @calc_loop_slow
;
// на повтор, если не конец.
@done_slow
:
xor    eax
,
0xFFFFFFFF
;
// финальный "XOR out" на выходе (см.таблицу выше)
pop    edx ecx ebx   
ret

Недостатком данного метода является XOR над каждым из 8-битов байта. Так, если блок данных имеет размер 1 МБ, то придётся прокрутить 8 млн.операций, что несомненно окажет отрицательное влияние на скорость выполнения расчёта. Поэтому алго побитового вычисления CRC32 рекомендуют для небольших объёмов данных, а в идеале нужно использовать т.н. "Табличный метод".

3.CRC32 с использованием таблиц

Таблица играет ключевую роль в оптимизации вычисления CRC - разберём её подробно.
Значение байта данных может лежать в диапазоне 0-255. Суть в том, что мы заранее в цикле вычисляем CRC32 для всех возможных значений 0-255, и помещаем эти шаблоны в таблицу, которая займёт в памяти
Код:

256х4=1КБ
. Теперь когда будем читать поток данных по-байтно, то значение этого байта можно будет использовать в качестве (!)индекса в таблице (порядкового номера), чтобы выбрать из неё уже предвычисленное значение CRC. Как результат мы избавляемся от проходов по всем 8-битам байта, что увеличит скорость в ~8 раз. Табличный метод оправдывает себя на больших массивах данных, и его нецелесообразно использовать, например, для строки "Hello World!". Вот как это дело реализовать на практике:

C-подобный:


Код:

;
//=============================
;
// Для начала создаём таблицу
;
//=============================
init_crc_table
:
mov    ecx
,
0
;
// для всех байтов от 0 до 255
@create
:
mov    eax
,
ecx
;
// текущий байт
;
// Обрабатываем по 8-бит (как в медленном методе)
mov    edx
,
8
@process_bit
:
shr    eax
,
1
jnc    @skip_xor
    xor    eax
,
0xEDB88320
@skip_xor
:
dec    edx
    jnz    @process_bit
;
// Сохраняем результат для этого байта в таблице
mov
[
crc_table
+
ecx
*
4
]
,
eax
 
    inc    ecx
    cmp    ecx
,
256
jb    @create

Теперь у нас есть таблица, и можно выбирать из неё значения CRC32 для каждого из возможных байтов так. Адресация вида
Код:

esi+ebx*4
называется базово-индексной с масштабированием (Base-Indexed with Scale), или адресацией SIB (Scale-Index-Base). Она используется для доступа к элементам массивов, где
Код:

esi
= базовый адрес,
Код:

ebx
= индекс, а
Код:

4
= масштаб (размер элемента 4 байта):

C-подобный:


Код:

calc_crc32_fast
:
mov    eax
,
0xFFFFFFFF
@calc_loop
:
movzx  ebx
,
byte
[
esi
]
;
// 1. загружаем байт
xor    bl
,
al
;
// 2. xor с младшим байтом CRC
movzx  ebx
,
bl
;
// 3. индекс в таблице (0-255)
shr    eax
,
8
;
// 4. сдвигаем CRC
xor    eax
,
[
crc_table
+
ebx
*
4
]
;
// 5. xor с табличным значением
inc    esi
;
// следующий байт
dec    ecx
;
// конец данных?
jnz    @calc_loop
;
// нет - продолжить.
xor    eax
,
0xFFFFFFFF
;
// финальный "xor out"

Для лучшего понимания проведём аналогию.
Пусть нам нужно умножить числа от 0 до 255 на 43:

Цитата:


Без таблицы: каждый раз вычислять x *43
С таблицей : создать массив table[x] = x *43, и потом просто брать готовое значение


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

4. Как на выходе получитьCRC32 = нуль

У обоих методов выше (по-битовый, табличный) есть один общий недостаток - если мы хотим периодически проверять контрольную сумму блока данных, то оригинальный CRC приходится где-то хранить, что не есть гуд. Поэтому после повторного пересчёта, CRC данных в идеале должна быть равна нулю, тогда и оригинал будет не нужен.

Учитывая, что в основе алгоритма CRC лежит XOR, получить на выходе нуль довольно легко - для этого достаточно пересчитать сумму блока как обычно, после чего применив операцию
Код:

not crc32
, добавить инверсный CRC в хвост блока данных, младшим байтом вперёд Little-Endian. Если после этого обратно пересчитать CRC с учётом добавленного дворда, то получим нуль, что собственно и требовалось доказать. Вот пример:

C-подобный:


Код:

format pe console
entry start
include
'win32ax.inc'
section
'.data'
data readable writeable
crc_table      dd
256
dup
(
0
)
test_string    db
'Hello, World!'
,
0
test_len
=
$
-
test_string
-
1
data_with_crc  rb
256
;
// буфер для данных + CRC
;
//----------
section
'.code'
code readable executable
;
// ======================================
;
// Инициализация таблицы CRC32
;
// ======================================
init_crc_table
:
push    ebx ecx edx
      xor    ecx
,
ecx
;
// байт(0) из 256
@generate_table
:
mov    eax
,
ecx
;
// текущий байт
mov    edx
,
8
;
// счётчик бит
@process_bit
:
shr    eax
,
1
;
// сдвиг на 1-бит вправо
jnc    @skip_xor
;
// если бит не был установлен
xor    eax
,
0xEDB88320
;
// иначе ксор с полиномом
@skip_xor
:
dec    edx
;
// повтор для всех 8-бит
jnz    @process_bit
 
      mov
[
crc_table
+
ecx
*
4
]
,
eax
;
// сохраняем в таблицу
inc    ecx
      cmp    ecx
,
256
;
// заполняем всю таблицу до 256 двордов
jb      @generate_table
      pop    edx ecx ebx
ret
;
//==================================
;
// Вычисление CRC32
;
// Вход: ESI - данные, ECX - длина
;
// Выход: EAX - CRC32
;
//==================================
align
16
calc_crc32
:
push    ebx edx esi ecx
      mov    eax
,
0xFFFFFFFF
;
// начальное значение CRC-32
or      ecx
,
ecx
      jz      @done
@calc_loop
:
movzx  ebx
,
byte
[
esi
]
;
// берём очередной байт данных
xor    bl
,
al
;
// XOR с младшим байтом CRC
movzx  ebx
,
bl
;
// расширяем до 32-бит
shr    eax
,
8
;
// сдвиг CRC вправо на 8-бит
xor    eax
,
[
crc_table
+
ebx
*
4
]
;
// XOR с значением из таблицы!
inc    esi
;
// следующий байт данных..
dec    ecx
;
// это конец данных?
jnz    @calc_loop
;
// нет на повтор
@done
:
xor    eax
,
0xFFFFFFFF
;
// финальный: CRC32 xor -1
pop    ecx esi edx ebx
ret
;
//===============================
;
//  Точка входа в программу
;
//===============================
align
16
start
:
call    init_crc_table
      cinvoke  printf
,

,
test_len
;
// 1. Вычисляем CRC для исходной строки
mov    esi
,
test_string
      mov    ecx
,
test_len
      call    calc_crc32
      push    eax

      cinvoke  printf
,

,
eax
;
// 2. Копируем данные в буфер (без нуль-терминатора)
mov    esi
,
test_string
      mov    edi
,
data_with_crc
      mov    ecx
,
test_len
      rep    movsb
;
// 3. Добавляем CRC в конец данных
pop    eax
      not    eax
;
// обязательная инверсия!
mov
[
edi
]
,
eax
;
// записываем 4 байта CRC
;
// 4. Обратно вычисляем CRC теперь для всего буфера
cinvoke  printf
,

,
test_len
+
4
mov    esi
,
data_with_crc
      mov    ecx
,
test_len
+
4
call    calc_crc32
      xor    eax
,
-
1
;
// 0xFFFFFFFF
cinvoke  printf
,

,
eax

      cinvoke  getch
      cinvoke  exit
,
0
;
//-------------
section
'.idata'
import data readable writeable
library msvcrt
,
'msvcrt.dll'
import  msvcrt
,
printf
,
'printf'
,
getch
,
'_getch'
,
exit
,
'exit'

https://forum.antichat.xyz/attachmen...9d2a2ae251.png

5. ПрименениеCRC32 на практике

Рассмотрим пример, когда алгоритмом CRC32 охраняется критический блок в секции-кода нашего приложения. Такой расклад можно наблюдать, если взломщик планирует обратить условие перехода
Код:

JZ
на
Код:

JNZ
при проверке, скажем, пароля. Значит пишем обычное приложение для запроса секретного слова (точнее суммы его байт, аля хэша), а в дополнительном потоке с периодом 1-сек будем пересчитывать контрольную сумму охраняемого периметра (защита от on-line патча). Если получим CRC32 отличную от нуля, сразу включаем сирену, выводим мессагу и на выход. Чтобы реализовать эту схему, нужно:

1. Встроить в код отладочную процедуру, которая покажет исходную сумму CRC32(1).
2. Теперь делаем not crc32(1) и записываем получившееся значение в хвост блока данных.

C-подобный:


Код:

format pe console
entry start
include
'win32ax.inc'
section
'.data'
data readable writeable
crc_table  dd
256
dup
(
0
)
pass        dd
'C'
+
'o'
+
'd'
+
'e'
+
'b'
+
'y'
;
// хэш пароля
align
16
buff        db
0
;
//----------
section
'.code'
code readable executable
;
//================================
;
// Инициализация таблицы CRC32
;
//================================
init_crc_table
:
push    ebx ecx edx
      xor    ecx
,
ecx
;
// байт(0) из 256
@generate_table
:
mov    eax
,
ecx
;
// текущий байт
mov    edx
,
8
;
// счётчик бит
@process_bit
:
shr    eax
,
1
;
// сдвиг на 1-бит вправо
jnc    @skip_xor
;
// если бит не был установлен
xor    eax
,
0xEDB88320
;
// иначе ксор с полиномом
@skip_xor
:
dec    edx
;
// повтор для всех 8-бит
jnz    @process_bit
 
      mov
[
crc_table
+
ecx
*
4
]
,
eax
;
// сохраняем в таблицу
inc    ecx
      cmp    ecx
,
256
;
// заполняем всю таблицу до 256 двордов
jb      @generate_table
      pop    edx ecx ebx
ret
;
//==================================
;
// Вычисление CRC32
;
// Вход: ESI - данные, ECX - длина
;
// Выход: EAX - CRC32
;
//==================================
align
16
calc_crc32
:
push    ebx edx esi ecx
      mov    eax
,
0xFFFFFFFF
;
// начальное значение CRC-32
or      ecx
,
ecx
      jz      @done
@calc_loop
:
movzx  ebx
,
byte
[
esi
]
;
// берём очередной байт данных
xor    bl
,
al
;
// XOR с младшим байтом CRC
movzx  ebx
,
bl
;
// расширяем до 32-бит
shr    eax
,
8
;
// сдвиг CRC вправо на 8-бит
xor    eax
,
[
crc_table
+
ebx
*
4
]
;
// XOR с значением из таблицы!
inc    esi
;
// следующий байт данных..
dec    ecx
;
// это конец данных?
jnz    @calc_loop
;
// нет на повтор
@done
:
xor    eax
,
0xFFFFFFFF
;
// финальный: CRC32 xor -1
pop    ecx esi edx ebx
ret
;
//==============================
;
// Поток для проверки CRC32
;
//==============================
proc CheckCrcThread lpParam
@@
:
invoke  Sleep
,
1000
mov    esi
,
@testPass
      mov    ecx
,
@endBlock
-
@testPass
      add    ecx
,
4
call    calc_crc32
      not    eax
      or      eax
,
eax
      jz      @b

      invoke  MessageBox
,
0
,

,
0
,
0
invoke  ExitThread
,
0
ret
endp
;
//===============================
;
//  Точка входа в программу
;
//===============================
align
16
start
:
call    init_crc_table

      mov    esi
,
@testPass
;
//
,
eax

      invoke  CreateThread
,
0
,
0
,
CheckCrcThread
,
0
,
0
,
0
;
// создаём поток
cinvoke  printf
,

xor    ebx
,
ebx
      mov    eax
,
ebx
@@
:
cinvoke  getch
;
// принимаем пасс у юзера
cmp    al
,
13
;
// это Enter?
je      @stop
;
// да - на выход
add    ebx
,
eax
;
// считаем сумму
jmp    @b
;
// продолжить..
@stop
:
cmp    ebx
,
[
pass
]
;
//
jmp    @end
@
01
:
cinvoke  printf
,

@end
:
cinvoke  getch
      cinvoke  exit
,
0
@endBlock
:
;
//*****************************************
;
// Конец охранной зоны
;
//*****************************************
section
'.idata'
import data readable writeable
library kernel32
,
'kernel32.dll'
,
user32
,
'user32.dll'
,
msvcrt
,
'msvcrt.dll'
include
'api\kernel32.inc'
include
'api\user32.inc'
import  msvcrt
,
printf
,
'printf'
,
scanf
,
'scanf'
,
\
                getch
,
'_getch'
,
exit
,
'exit'

Если собрать сейчас этот исходник по F9 в окне fasm'a, то получим мессагу, мол в охраняемую зону проник нарушитель. Оно и ясно, ведь доп.поток приложения проверяет CRC на нуль, и пока мы не добавим исходный "CRC32 value" в хвост блока (конец охранной зоны), окно с ошибкой так и будет всплывать.

Пропатчить экзешник можно прямо в отладчике x64dbg. Значит открываем бинарь, и спустившись до
Код:

call exit
жмём пробел = "Ассемблирование". В появившемся окне вводим значение дворда
Код:

dd 8b9116f6 --> OK
, и далее применим патч комбинацией
Код:

Ctrl+P
. Сохранять пропатченный EXE нужно под другим именем, и если потом запустить его, то окно с ошибкой уже пропадёт.

Процесс добавления CRC32 в хвост кода

https://forum.antichat.xyz/attachmen...6184e20085.png

Окно применения патча по Ctrl+P

https://forum.antichat.xyz/attachmen...8ded4dc748.png
Теперь можно сказать, что код нашей прожки защищён от модификации, т.к. доп.поток будет в цикле пересчитывать его контрольную сумму CRC32. Попробуйте по аналогичной схеме изменить сейчас
Код:

JZ
на
Код:

JNZ
после ввода пароля (метка @testPass), как тут-же всплывёт мессага выше с варнингом.

6. Послесловие

Под занавес приведём пример расчёта CRC32 аппаратным способом через инструкцию SSE4.2
Код:

crc32
. На каждый байт она расходует всего 1-такт, что позволяет получить скорость гепарда. Вот пример:

C-подобный:


Код:

;
// Проверка поддержки CRC32 инструкций
check_crc32_support
:
push  ebx
    mov    eax
,
1
cpuid
    test  ecx
,
(
1
<
<
20
)
;
// бит 20 - поддержка SSE4.2
jnz    @supported
    pop    ebx
    xor    eax
,
eax
;
// нет поддержки
ret
@supported
:
pop    ebx
    mov    eax
,
1
ret
;
//-----------------
;
// Аппаратное вычисление CRC32 (только для SSE4.2)
;
// На входе: ESI = адрес начала блока, ECX = длина
;
//-----------------
calc_crc32_hardware
:
push  ebx
    mov    eax
,
0xFFFFFFFF
;
// начальное значение
or    ecx
,
ecx
    jz    @done_hw
 
@calc_hw_loop
:
movzx  ebx
,
byte
[
esi
]
crc32  eax
,
bl
;
// аппаратная инструкция CRC32
inc    esi
    dec    ecx
    jnz    @calc_hw_loop
 
@done_hw
:
xor    eax
,
0xFFFFFFFF
pop    ebx
ret

Линк по теме:
Неплохой обзор алгоритма CRC32
Демистификация CRC32 | arm | programming

Gemfory 01.04.2026 20:58

Прикольная тема, только из этой статьи узнал про это.
Получается можно реализовать анти-патчинг/анти-хук программы через вычисление CRC32, то есть заставить программу проверятьсаму себя на наличие правок прямо во время работы. Условно замена jz на jmp как было показано в статье, или вставка инлайн хука jmp-шкой на свою функцию

Marylin 02.04.2026 05:50

Gemfory всё верно.
Более того можно и подделывать CRC, т.е. добавили данные, а CRC остался тот-же (если он проверяется не на нуль) - например у PEiD есть для этого спец.плагин CRC32, хотя можно и вручную/брутфорсом подобрать XYZW-байты.


Время: 17:52