ANTICHEAT_OBFUSCATE_CODEMARKER
-
Автор темы
- #1
СОДЕРЖАНИЕ:
Введение
1. CRC секций
2. CRC file
3. CRC disk
4. CRC static
Заключение
Введение
⠀⠀⠀⠀⠀Зачастую при реверсе программ под протекторами, да и просто самописной защитой, часто встречается проверки целостности. Не смотря на проблему, мало кто делится информацией, а тем более пытается понять смысл защиты и реализовать для себя её логику. Довольно забавно,но некоторые протекторы реализуют лишь небольшую часть проверок целостности(проверку одной секции при вызове SDK API или проверка только кода протектора с обфусцированным кодом, но не код без обфускации).
⠀⠀⠀⠀⠀Уже были разобраны обходы проверки целостности у протекторов в другой статье, поэтому предлагаю рассмотреть с противоположного угла саму проблему т.е как реализуется сами проверки целостности. Приведённый код в статье не будут очень большими и это просто пример для начинающих реверсеров. Заранее извиняюсь за не супер хороший стиль кода(если будет что-то непонятно, то сверху перед кодом будет написана логики реализации или можете написать под комментарием сам вопрос).
⠀⠀⠀⠀⠀Целью статьи является изучение основных проверок целостности кода и их реализация, включая их достоинства или недостатки. Так же будет сказано про уловки или какую головную боль может вызвать у реверсера сочетания проверок целостности кода.
1.CRC секций(runtime)
⠀⠀⠀⠀⠀Самое простое в реализации – CRC проверка секций. Обычно используют только как самописную защиту. Нас интересует пройтись по всем секциям нашего модуля и проверить Characteristics на IMAGE_SCN_MEM_READ и !IMAGE_SCN_MEM_WRITE. Дальше мы можем получить crc ,основываясь на VirtualAddress и VirtualSize:

⠀⠀⠀⠀⠀Размер секции можно по желанию выровнять по SectionAlignment,чтобы не оставлять выровненный размер без проверки,но это необязательно.
Само получение можно реализовать следующим образом:
C++:
NO_INLINE auto add_crc_module(PVOID addr_mod, BOOLEAN is_copy = FALSE, BOOLEAN steals_check = FALSE) -> BOOLEAN
{
BOOLEAN is_any_add = FALSE;
PIMAGE_NT_HEADERS nt_headers = NULL;
PIMAGE_SECTION_HEADER sec = NULL;
CRC_INFO crc_info = { NULL };
nt_headers = reinterpret_cast<IMAGE_NT_HEADERS*>(reinterpret_cast<CHAR*>(addr_mod) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_mod)->e_lfanew);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE)
return is_any_add;
sec = IMAGE_FIRST_SECTION(nt_headers);
for (uint16_t i = NULL, check_sec = NULL; i <= nt_headers->FileHeader.NumberOfSections; i++)
{
if ((sec[i].Characteristics & IMAGE_SCN_MEM_READ) && !(sec[i].Characteristics & IMAGE_SCN_MEM_WRITE))
{
//flag
crc_info.crc_is_copy = is_copy;
crc_info.crc_steals_check = steals_check;
//info
crc_info.crc_addr = reinterpret_cast<uint8_t*>(addr_mod) + sec[i].VirtualAddress;
crc_info.crc_size = sec[i].Misc.VirtualSize;
crc_info.crc_coped_mem = NULL;
if (crc_info.crc_is_copy || crc_info.crc_steals_check)
{
crc_info.crc_coped_mem = reinterpret_cast<uint8_t*>(VirtualAlloc(NULL, crc_info.crc_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (crc_info.crc_coped_mem)
{
if (!ReadProcessMemory(NtCurrentProcess, crc_info.crc_addr, crc_info.crc_coped_mem, crc_info.crc_size, NULL))
{
VirtualFree(crc_info.crc_coped_mem, NULL, MEM_RELEASE);
crc_info.crc_coped_mem = NULL;
}
else
{
//short time change(we init)
crc_info.crc_addr = crc_info.crc_coped_mem;
crc_info.crc_res = crc32(&crc_info);
crc_info.crc_addr = reinterpret_cast<uint8_t*>(addr_mod) + sec[i].VirtualAddress;
is_any_add = TRUE;
}
}
}
if (!crc_info.crc_steals_check)
{
crc_info.crc_res = crc32(&crc_info);
is_any_add = TRUE;
}
crc_list.push_back(crc_info);
}
}
return is_any_add;
}
C++:
NO_INLINE auto sec_check() -> BOOLEAN
{
uint8_t* crc_coped = NULL;
CRC_INFO crc_info = { NULL };
for (size_t i = 0; i < crc_list.size(); i++)
{
if (crc_list[i].crc_steals_check)
{
memcpy(&crc_info, &crc_list[i], sizeof(crc_info));
crc_coped = reinterpret_cast<uint8_t*>(VirtualAlloc(NULL, crc_info.crc_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (crc_coped)
{
if (ReadProcessMemory(NtCurrentProcess, crc_info.crc_addr, crc_coped, crc_info.crc_size, NULL))
{
crc_info.crc_addr = crc_coped;
if (crc32(&crc_info) != crc_list[i].crc_res)
{
if (crc_list[i].crc_is_copy && crc_list[i].crc_coped_mem)
{
crc_unhook_code(&crc_list[i]);
}
return TRUE;
}
}
else
{
if (crc32(&crc_list[i]) != crc_list[i].crc_res)
{
if (crc_list[i].crc_is_copy && crc_list[i].crc_coped_mem)
{
crc_unhook_code(&crc_list[i]);
}
return TRUE;
}
}
VirtualFree(crc_coped, NULL, MEM_RELEASE);
}
else
{
if (crc32(&crc_list[i]) != crc_list[i].crc_res)
{
if (crc_list[i].crc_is_copy && crc_list[i].crc_coped_mem)
{
crc_unhook_code(&crc_list[i]);
}
return TRUE;
}
}
}
else
{
if (crc32(&crc_list[i]) != crc_list[i].crc_res)
{
if (crc_list[i].crc_is_copy && crc_list[i].crc_coped_mem)
{
crc_unhook_code(&crc_list[i]);
}
return TRUE;
}
}
}
return FALSE;
}
Из плюсов:
Из минусов:Возможность произвести unhook байтов(скопированные байты лучше хранить в зашифрованном виде или сжать их).
Можно спокойно реализовать скрытное чтение,чтобы избежать быстрое обнаружение тем же HWBP.
Имеет смысл использовать с сочетанием SDK протектора,поскольку некоторые моменты могут перекрывать друг друга(приводить примера не буду,но подумайте сами).
Получение происходит после вызова OEP.
Много способов обойти:уменьшить количество секций, подмена. Characteristics, подмена VirtualAddress(например,на одну и ту же секцию,которую мы не изменяем) или VirtualSize(уменьшить до NULL) и т.д
Хукнуть начала получения и вернуть адрес скопированного модуля.
2.CRC file(static)
⠀⠀⠀⠀⠀Проверка целостности файла на диске – пожалуй, можно назвать, наиболее распространённой проверкой у протекторов. Единственный плюс такой проверки – обнаружение статического патча бинарника и … на этом всё.
⠀⠀⠀⠀⠀Сама проверка не супер сложна в реализации и приводится пример с учётом изменения только данных в секциях бинарника,но без проверки самого PE(SizeOfHeaders),но можете это сами исправить т.к делается относительно легко. Можно сделать подсчёт по адресу(PointerToRawData) и размеру(SizeOfRawData) секций на диске или от начала расчёта(sec[NULL].PointerToRawData) до конца самого размера (с учётом игнорирования памяти, куда будет сохранять результат).Если место куда сохраняем начинается с 1 байт 1 секции,то нужно пропустить эти байти и тогда у нас будет 1 crc res, иначе 2.
Сам код подсчёта:
Код:
auto disk_calc_file_crc(PVOID addr_file,uint32_t size_file, uint8_t* addr_pattern) -> BOOLEAN
{
PIMAGE_NT_HEADERS nt_headers = NULL;
PIMAGE_SECTION_HEADER sec = NULL;
CRC_FILE_INFO crc_file_info = {NULL};
nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_file) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_file)->e_lfanew);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
sec = IMAGE_FIRST_SECTION(nt_headers);
if (addr_pattern - addr_file == nt_headers->OptionalHeader.SizeOfHeaders)//skip pe and next start sec - pattern
{
crc_file_info.crc_reg[NULL].crc_addr_rva = nt_headers->OptionalHeader.SizeOfHeaders + sizeof(crc_file_info);
crc_file_info.crc_reg[NULL].crc_size = size_file - nt_headers->OptionalHeader.SizeOfHeaders - sizeof(crc_file_info);
crc_file_info.crc_reg[NULL].crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + crc_file_info.crc_reg[NULL].crc_addr_rva, crc_file_info.crc_reg[NULL].crc_size);
crc_file_info.crc_num = 1;
}
else
{
crc_file_info.crc_reg[NULL].crc_addr_rva = nt_headers->OptionalHeader.SizeOfHeaders;
crc_file_info.crc_reg[NULL].crc_size = (addr_pattern - addr_file) - nt_headers->OptionalHeader.SizeOfHeaders;
crc_file_info.crc_reg[NULL].crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + crc_file_info.crc_reg[NULL].crc_addr_rva, crc_file_info.crc_reg[NULL].crc_size);
crc_file_info.crc_reg[1].crc_addr_rva = (addr_pattern - addr_file) + sizeof(crc_file_info);
crc_file_info.crc_reg[1].crc_size = size_file - (addr_pattern - addr_file) - sizeof(crc_file_info);
crc_file_info.crc_reg[1].crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + crc_file_info.crc_reg[1].crc_addr_rva, crc_file_info.crc_reg[1].crc_size);
crc_file_info.crc_num = 2;
}
reinterpret_cast<CRC_FILE_INFO*>(addr_pattern)->crc_num = crc_file_info.crc_num;
addr_pattern += sizeof(uint32_t);
for (size_t i = 0; i < crc_file_info.crc_num; i++)
{
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_addr_rva = crc_file_info.crc_reg[i].crc_addr_rva;
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_size = crc_file_info.crc_reg[i].crc_size;
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_res = crc_file_info.crc_reg[i].crc_res;
addr_pattern += sizeof(CRC_INFO_FILE);
}
return TRUE;
}
Сама проверка:
C++:
CRC_FILE_INFO crc_res_table = {0xBEEF,2,0XDEADC0DE};
auto crc_file_check(PVOID addr_file) -> BOOLEAN
{
PIMAGE_NT_HEADERS nt_headers = NULL;
PIMAGE_SECTION_HEADER sec = NULL;
nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_file) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_file)->e_lfanew);
sec = IMAGE_FIRST_SECTION(nt_headers);
//rva hear is va
for (size_t i = 0; i < crc_res_table.crc_num; i++)
{
if (crc32(reinterpret_cast<uint8_t*>(addr_file) + crc_res_table.crc_reg[i].crc_addr_rva, crc_res_table.crc_reg[i].crc_size) != crc_res_table.crc_reg[i].crc_res)
{
return FALSE;
}
}
return TRUE;
}
auto is_file_valid(PVOID mod_address) -> BOOLEAN
{
BOOLEAN is_valid = TRUE;
uint32_t old_size_file = NULL;
DWORD num_read = NULL;
PVOID memory_module = NULL;
PVOID memory_file = NULL;
HANDLE access_file = NULL;
WCHAR file_path[MAX_PATH] = { NULL };
if (mod_address && GetModuleFileNameW(reinterpret_cast<HMODULE>(mod_address), file_path, sizeof(file_path)))
{
access_file = CreateFileW(file_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (access_file != INVALID_HANDLE_VALUE)
{
old_size_file = GetFileSize(access_file, NULL);
if (old_size_file)
{
memory_file = malloc(old_size_file);
if (memory_file)
{
memset(memory_file, NULL, old_size_file);
if (ReadFile(access_file, memory_file, old_size_file, &num_read, NULL))
{
is_valid = crc_file_check(memory_file);
}
free(memory_file);
}
}
CloseHandle(access_file);
}
}
return is_valid;
}
Зачастую для такой проверки достаточно просто подменить путь на оригинальный скопированный файл при обращении к файлам на диске(как было с обходом Themida
Пожалуйста, авторизуйтесь для просмотра ссылки.
из прошлой статьи).Из плюсов:
Из минусов:Статический детект патча байтов в бинарнике.
Можно совмещать с другим примеров(слегка ниже),чтобы слегка усложнит простую подделку с путём подделки на скопированный бинарник.
Легко обходится.
Требует загрузку файла с диска
Можно игнорировать, если используете динамический патч(играть честно,когда все играют краплёными картами – задумка не очень).
3.CRC disk(runtime)
⠀⠀⠀⠀⠀Можно не пытаться устраивать танцы с бубнами и просто загрузить файл с диска, пофиксить все данные (обычно достаточно исправить IMAGE_DIRECTORY_ENTRY_BASERELOC и IMAGE_DIRECTORY_ENTRY_IMPORT), которые изменяются при загрузки бинарника и сравнить с текущей памятью(можно сказать,просто частичное повторение manual map injector). Можно совместить с CRC file и это не позволит лениво обойти это(подмена файла( т.к память будет отличаться,если просто подменить путь к оригинальному файлу. Нужно проверять только секции с Characteristics на IMAGE_SCN_MEM_READ и !IMAGE_SCN_MEM_WRITE. Для файла с диска проверяемосновываясь на PointerToRawData,а на файле VirtualAddress из-за выравнивания PE и секций кода,но должны использовать SizeOfRawData. Протекторы не используют данную технику,но некоторые AC используют данный трюк.
Реализация довольно тривиальна:
C++:
auto disk_fix(PVOID addr_file, PVOID addr_mod) -> BOOLEAN
{
uint32_t end_reloce = NULL;
uint64_t delta_fix = NULL;
HMODULE base_imp_dll = NULL;
CHAR* name_imp_dll = NULL;
uint16_t* relative_info = NULL;
uint64_t* orig_first_thunk = NULL;
uint64_t* first_thunk = NULL;
PIMAGE_NT_HEADERS nt_headers = NULL;
PIMAGE_BASE_RELOCATION va_reloce = NULL;
PIMAGE_BASE_RELOCATION size_reloce = NULL;
PIMAGE_SECTION_HEADER sec = NULL;
PIMAGE_IMPORT_DESCRIPTOR imp_descript = NULL;
PIMAGE_IMPORT_BY_NAME import_name = NULL;
nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_file) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_file)->e_lfanew);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
sec = IMAGE_FIRST_SECTION(nt_headers);
delta_fix = reinterpret_cast<uint64_t>(addr_mod) - nt_headers->OptionalHeader.ImageBase;
if ( delta_fix)
{
if (nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size)
{
va_reloce = reinterpret_cast<PIMAGE_BASE_RELOCATION>(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress, nt_headers,sec));
size_reloce = reinterpret_cast<PIMAGE_BASE_RELOCATION>(reinterpret_cast<uintptr_t>(va_reloce) + nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size);
while (va_reloce < size_reloce && va_reloce->SizeOfBlock)
{
end_reloce = (va_reloce->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
relative_info = reinterpret_cast<uint16_t*>(va_reloce + 1);
for (uint32_t i = NULL; i != end_reloce; ++i, ++relative_info)
{
switch ((*relative_info) >> SHIFT_RELOCE)//https://github.com/DarthTon/Blackbone/blob/5ede6ce50cd8ad34178bfa6cae05768ff6b3859b/src/BlackBoneDrv/ldrreloc.c#L252
{
case IMAGE_REL_BASED_DIR64:
{
*reinterpret_cast<uint64_t*>(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(va_reloce->VirtualAddress, nt_headers, sec) + ((*relative_info) & 0xFFF)) += delta_fix;
break;
}
case IMAGE_REL_BASED_HIGHLOW:
{
*reinterpret_cast<uint32_t*>(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(va_reloce->VirtualAddress, nt_headers, sec) + ((*relative_info) & 0xFFF)) += delta_fix;
break;
}
case IMAGE_REL_BASED_LOW:
{
*reinterpret_cast<uint16_t*>(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(va_reloce->VirtualAddress, nt_headers, sec) + ((*relative_info) & 0xFFF)) += delta_fix;
break;
}
case IMAGE_REL_BASED_ABSOLUTE:
{
break;
}
default:
{
return FALSE;
break;
}
}
}
va_reloce = reinterpret_cast<PIMAGE_BASE_RELOCATION>(reinterpret_cast<BYTE*>(va_reloce) + va_reloce->SizeOfBlock);
}
}
if (nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size)
{
imp_descript = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(reinterpret_cast<CHAR*>(addr_file) + rva_to_va(nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress, nt_headers, sec));
for (; imp_descript->Name; ++imp_descript)
{
name_imp_dll = reinterpret_cast<CHAR*>(addr_file) + rva_to_va(imp_descript->Name,nt_headers,sec);
if (name_imp_dll)
{
base_imp_dll = LoadLibraryA(name_imp_dll);
if (base_imp_dll)
{
orig_first_thunk = reinterpret_cast<uint64_t*>(reinterpret_cast<CHAR*>(addr_file) + rva_to_va(imp_descript->OriginalFirstThunk,nt_headers,sec));
first_thunk = reinterpret_cast<uint64_t*>(reinterpret_cast<CHAR*>(addr_file) + rva_to_va(imp_descript->FirstThunk, nt_headers, sec));
if (!orig_first_thunk) //load by index https://stackoverflow.com/questions/42413937/why-pe-need-original-first-thunkoft
{
orig_first_thunk = first_thunk;
}
for (; *orig_first_thunk; orig_first_thunk++, first_thunk++)
{
if (IMAGE_SNAP_BY_ORDINAL(*orig_first_thunk))//load by index?
{
*first_thunk = reinterpret_cast<uint64_t>(GetProcAddress(base_imp_dll, reinterpret_cast<CHAR*>(IMAGE_ORDINAL(*orig_first_thunk))));
}
else
{
import_name = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(reinterpret_cast<CHAR*>(addr_file) + rva_to_va(*orig_first_thunk,nt_headers,sec));
if (import_name->Name)
{
*first_thunk = reinterpret_cast<uint64_t>(GetProcAddress(base_imp_dll, import_name->Name));
}
}
}
}
}
}
}
}
return TRUE;
}
auto disk_compare(PVOID addr_file, PVOID addr_mod) -> BOOLEAN
{
PIMAGE_NT_HEADERS nt_headers_disk = NULL;
PIMAGE_SECTION_HEADER sec_disk = NULL;
PIMAGE_NT_HEADERS nt_headers_memory = NULL;
PIMAGE_SECTION_HEADER sec_memory = NULL;
nt_headers_disk = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_file) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_file)->e_lfanew);
nt_headers_memory = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_mod) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_mod)->e_lfanew);
if
(
nt_headers_disk->Signature != IMAGE_NT_SIGNATURE ||
nt_headers_memory->Signature != IMAGE_NT_SIGNATURE ||
nt_headers_disk->FileHeader.NumberOfSections != nt_headers_memory->FileHeader.NumberOfSections
)
{
return FALSE;
}
sec_disk = IMAGE_FIRST_SECTION(nt_headers_disk);
sec_memory = IMAGE_FIRST_SECTION(nt_headers_memory);
for (uint16_t i = NULL, check_sec = NULL; i <= nt_headers_disk->FileHeader.NumberOfSections; i++)
{
if (sec_disk->Characteristics != sec_memory->Characteristics)
{
return FALSE;
}
if (sec_disk[i].PointerToRawData)
{
if (sec_disk[i].VirtualAddress != sec_memory[i].VirtualAddress || sec_disk[i].PointerToRawData != sec_memory[i].PointerToRawData || sec_disk[i].SizeOfRawData != sec_memory[i].SizeOfRawData)
{
return FALSE;
}
if ((sec_disk[i].Characteristics & IMAGE_SCN_MEM_READ) && !(sec_disk[i].Characteristics & IMAGE_SCN_MEM_WRITE))
{
if (
crc32(reinterpret_cast<uint8_t*>(addr_file) + sec_disk[i].PointerToRawData, sec_disk[i].SizeOfRawData) !=
crc32(reinterpret_cast<uint8_t*>(addr_mod) + sec_memory[i].VirtualAddress, sec_memory[i].SizeOfRawData)
)
{
return FALSE;
}
}
}
}
return TRUE;
}
auto is_file_valid(PVOID mod_address) -> BOOLEAN
{
BOOLEAN is_valid = TRUE;
uint32_t old_size_file = NULL;
DWORD num_read = NULL;
PVOID memory_module = NULL;
PVOID memory_file = NULL;
HANDLE access_file = NULL;
WCHAR file_path[MAX_PATH] = { NULL };
if (mod_address && GetModuleFileNameW(reinterpret_cast<HMODULE>(mod_address), file_path, sizeof(file_path)))
{
access_file = CreateFileW(file_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (access_file != INVALID_HANDLE_VALUE)
{
old_size_file = GetFileSize(access_file, NULL);
if (old_size_file)
{
memory_file = malloc(old_size_file);
if (memory_file)
{
memset(memory_file, NULL, old_size_file);
if (ReadFile(access_file, memory_file, old_size_file, &num_read, NULL))
{
if (disk_fix(memory_file, mod_address))
{
is_valid = disk_compare(memory_file, mod_address);
}
}
free(memory_file);
}
}
CloseHandle(access_file);
}
}
return is_valid;
}
Из минусов:Можно параллельно проверить CRC file
Основывается только на файле,который находится на диске и на данных,котоыре мы должны исправить
Требует загрузку файла с диска
Нужно исправить все данные,которые меняются при загрузке бинарника
Для некоторых файлах нужно исключить секции при проверки(особенно, если секция была упакована).
4.CRC static
⠀⠀⠀⠀⠀Вместо подсчёта на основе загрузки файла или когда система всё пофиксит, можно просто рассчитать статические данные, которые не меняются при загрузке бинарника. Такой подход является,на взгляд автора,наиболее правильным и должен присутствовать в любом протекторе. К удивлению, некоторые протекторы не реализуют его или реализуют частично (проверка 1 секции .text или только свой обфусцированный код). Нужно получить адреса и размер данных,которые изменяются при загрузке бинарника(в примере демонстрируется IMAGE_DIRECTORY_ENTRY_BASERELOC и IMAGE_DIRECTORY_ENTRY_IMPORT),чтобы отфильтровать по адресу. После этого пройтись по секциям и проверить Characteristics на IMAGE_SCN_MEM_READ и !IMAGE_SCN_MEM_WRITE. Если секция попадает под наши не менее жёсткие требования,то просто игнорировать сами данные и получить адрес и размер статических данных. Вот основные моменты,чтобы понять принцип работы:
- если в секции отсутствуют не статические данные – берём полный её полный размер.
- если в секции несколько данных, то берём начало и идём до конца секции.
- если секция начинается с не статических данных, то начинаем с окончанием не статических данных(адрес + размер) и идём до конца секции.
- если следующая нестатическая данная находится за пределами,то получаем размер = окончание секции - текущие статические данные.
- если присутствует только 1 не статическая данная, то берём начало секции до адреса этой данной и конце не статической данной до конца секции.
- 4 пункт, но с условием для 3 пункта.
Пожалуйста, авторизуйтесь для просмотра ссылки.
смотрите на Github:
C++:
auto disk_calc_static_crc(PVOID addr_file, uint8_t* addr_pattern) -> BOOLEAN
{
BOOLEAN fisrt_crc_init = FALSE;
BOOLEAN ignore_first_sec_byte = FALSE;
BOOLEAN any_crc_ignore = FALSE;
uint32_t end_reloce = NULL;
HMODULE base_imp_dll = NULL;
CHAR* name_imp_dll = NULL;
uint16_t* relative_info = NULL;
uint64_t* orig_first_thunk = NULL;
uint64_t* first_thunk = NULL;
PIMAGE_NT_HEADERS nt_headers = NULL;
PIMAGE_BASE_RELOCATION va_reloce = NULL;
PIMAGE_BASE_RELOCATION size_reloce = NULL;
PIMAGE_SECTION_HEADER sec = NULL;
PIMAGE_IMPORT_DESCRIPTOR imp_descript = NULL;
PIMAGE_IMPORT_BY_NAME import_name = NULL;
CRC_IGNORE_INFO crc_cur_ignore = { NULL };
CRC_INFO_FILE crc_cur_file = { NULL };
nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<CHAR*>(addr_file) + reinterpret_cast<IMAGE_DOS_HEADER*>(addr_file)->e_lfanew);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
sec = IMAGE_FIRST_SECTION(nt_headers);
//get reloce & import
std::sort(crc_ignore.begin(), crc_ignore.end(), [](CRC_IGNORE_INFO& cur, CRC_IGNORE_INFO& next){return cur.rva < next.rva; });
for (size_t i = NULL; i < nt_headers->FileHeader.NumberOfSections; i++)
{
fisrt_crc_init = FALSE;
if ((sec[i].Characteristics & IMAGE_SCN_MEM_READ) && !(sec[i].Characteristics & IMAGE_SCN_MEM_WRITE) && sec[i].SizeOfRawData > sizeof(PVOID))
{
any_crc_ignore = TRUE;
for (size_t j = NULL; j < crc_ignore.size(); j++)
{
if (crc_ignore[j].rva >= sec[i].VirtualAddress && (sec[i].VirtualAddress + sec[i].Misc.VirtualSize - sizeof(PVOID)) > crc_ignore[j].rva)
{
any_crc_ignore = FALSE;
break;
}
}
if (any_crc_ignore) //crc res all secthion
{
crc_cur_file.crc_addr_rva = sec[i].VirtualAddress;
crc_cur_file.crc_size = sec[i].SizeOfRawData;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
}
else //get crc
{
fisrt_crc_init = TRUE;
for (size_t j = NULL; j < crc_ignore.size(); j++)
{
if (crc_ignore[j].rva >= sec[i].VirtualAddress && (sec[i].VirtualAddress + sec[i].Misc.VirtualSize - sizeof(PVOID)) > crc_ignore[j].rva)
{
ignore_first_sec_byte = FALSE;
if (crc_ignore[j].rva == sec[i].VirtualAddress)
{
ignore_first_sec_byte = TRUE;
}
if (!ignore_first_sec_byte)
{
if (fisrt_crc_init)
{
if (j == NULL && crc_ignore[j].rva - sec[i].VirtualAddress > NULL)
{
if (crc_ignore.size() > j && crc_ignore[j + 1].rva - crc_ignore[j].rva > NULL)
{
crc_cur_file.crc_addr_rva = sec[i].VirtualAddress;
crc_cur_file.crc_size = crc_ignore[j].rva - sec[i].VirtualAddress;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
fisrt_crc_init = FALSE;
}
}
else
{
if (crc_ignore.size() > j && crc_ignore[j + 1].rva - (crc_ignore[j].rva + crc_ignore[j].size) > NULL)
{
if (crc_ignore[j + 1].rva >= sec[i].VirtualAddress && (sec[i].VirtualAddress + sec[i].Misc.VirtualSize) > crc_ignore[j + 1].rva)
{
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = crc_ignore[j + 1].rva - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
fisrt_crc_init = FALSE;
crc_res.push_back(crc_cur_file);
}
else
{
break;
}
}
else if (crc_ignore.size() == 1)
{
//not check code
crc_cur_file.crc_addr_rva = sec[i].VirtualAddress;
crc_cur_file.crc_size = crc_ignore[j].rva - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
//not check code
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = sec[i].SizeOfRawData - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
fisrt_crc_init = FALSE;
}
}
}
else
{
if (crc_ignore.size() > j)
{
if (crc_ignore[j + 1].rva - (crc_ignore[j].rva + crc_ignore[j].size) > NULL)
{
if (crc_ignore[j + 1].rva >= sec[i].VirtualAddress && (sec[i].VirtualAddress + sec[i].Misc.VirtualSize) > crc_ignore[j + 1].rva + crc_ignore[j + 1].size)
{
if (crc_ignore[j + 1].rva > crc_ignore[j].rva && crc_ignore[j + 1].rva > crc_ignore[j].rva + crc_ignore[j].size)
{
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = crc_ignore[j + 1].rva - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
fisrt_crc_init = FALSE;
crc_res.push_back(crc_cur_file);
}
}
else if (sec[i].VirtualAddress + sec[i].SizeOfRawData - (crc_ignore[j].rva + crc_ignore[j].size) > NULL)
{
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = sec[i].VirtualAddress + sec[i].SizeOfRawData - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
fisrt_crc_init = FALSE;
crc_res.push_back(crc_cur_file);
break;
}
else
{
break;
}
}
}
else if (crc_ignore.size() == 1)
{
__debugbreak();
}
}
}
else//start by cur and next rva
{
if (crc_ignore.size() > j)
{
if (crc_ignore[j + 1].rva - (crc_ignore[j].rva + crc_ignore[j].size) > NULL)
{
if (crc_ignore[j + 1].rva > crc_ignore[j].rva && crc_ignore[j + 1].rva > crc_ignore[j].rva + crc_ignore[j].size)
{
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = crc_ignore[j + 1].rva - crc_cur_file.crc_addr_rva;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
}
}
}
else if (crc_ignore.size() == 1)
{
//not check code
crc_cur_file.crc_addr_rva = crc_ignore[j].rva + crc_ignore[j].size;
crc_cur_file.crc_size = sec[i].VirtualAddress + sec[i].SizeOfRawData - crc_ignore[j].size;
crc_cur_file.crc_res = crc32(reinterpret_cast<uint8_t*>(addr_file) + rva_to_va(crc_cur_file.crc_addr_rva, nt_headers, sec), crc_cur_file.crc_size);
crc_res.push_back(crc_cur_file);
fisrt_crc_init = FALSE;
}
}
}
}
}
}
}
//copy size
reinterpret_cast<CRC_STATIC_INFO*>(addr_pattern)->crc_num = crc_res.size();
addr_pattern += sizeof(uint32_t);
for (size_t i = NULL; i < crc_res.size(); i++)
{
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_addr_rva = crc_res[i].crc_addr_rva;
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_res = crc_res[i].crc_res;
reinterpret_cast<CRC_INFO_FILE*>(addr_pattern)->crc_size = crc_res[i].crc_size;
addr_pattern += sizeof(CRC_INFO_FILE);
}
return TRUE;
}
Код:
CRC_STATIC_INFO crc_res_table = { 0xDEADC0DE, {0xBEEF,1,0XDEADC0DE} };
auto crc_static_check(PVOID addr_mod) -> BOOLEAN
{
for (size_t i = NULL; i < crc_res_table.crc_num; i++)
{
if (crc32(reinterpret_cast<uint8_t*>(addr_mod) + crc_res_table.crc_reg[i].crc_addr_rva, crc_res_table.crc_reg[i].crc_size) != crc_res_table.crc_reg[i].crc_res)
{
printf("rva ->\t%p\n", crc_res_table.crc_reg[i].crc_addr_rva);
printf("crc_res ->\t%p\n", crc_res_table.crc_reg[i].crc_res);
return FALSE;
}
}
return TRUE;
}
Из плюсов:
Из минусов:Буквально нужно только получить адрес проверяемого модуля и всё
Можно зашифровывать некоторые данные, основываясь на crc подсчитанный и расшифровывать на результате crc в памяти
Правильная реализация нахождения всех статических данных
Заключительные слова
⠀⠀⠀⠀⠀Нужно понимать,что многие проверки показаны не как 1337 защита,а просто пример для обучения. CRC является просто дополнительным слоем защиты и небольшой занозой в заднице. В тех же протекторах могут использовать результат рандомного CRC для расшифровки констант из-за чего может происходить поломка работы программы из-за тех же хуков/патчев. Надеюсь, эта статья принесла что-то новое читателю и спасибо за чтение статьи :).
Пожалуйста, авторизуйтесь для просмотра ссылки.
Пожалуйста, авторизуйтесь для просмотра ссылки.
Последнее редактирование: