Пользователь
- Статус
- Оффлайн
- Регистрация
- 12 Дек 2022
- Сообщения
- 131
- Реакции
- 46
Существуют хорошо известные функции, используемые в целях отладки и защиты:
~ Обнуление DebugPort
~ NtOpenProcess
~ NtOpenThread
~ KiAttachProcess
~ NtReadVirtualMemory
~ NtWriteVirtualMemory
Следующие две функции используются для обмена сообщениями между процессом и отладчиком. Если они не обрабатываются, отладчик не сможет получать или передавать информацию:
~ DbgkpSetProcessDebugObject
~ DbgkpQueueMessage
Эти функции уже многократно описаны в интернете, поэтому здесь я лишь кратко остановлюсь на них
На мой взгляд, обнуление DebugPort — один из самых сложных моментов в защите TP, при этом в сети об этом почти нет подробностей, а имеющиеся объяснения неполные
Начнём с двухмашинной отладки:
TP постоянно вызывает KdDisableDebugger, чтобы отключить отладку. Мы просто заставляем этот вызов сразу возвращаться, так как в начале функции нет проверок. По исходному коду WRK видно, что функция также обрабатывает связанные переменные и состояния отладки — их тоже нужно корректно подменить
При первом запуске TP отладчик останавливается на:
При втором запуске TP:
Таким образом, TP запускается дважды: первый запуск — ложный
Hook на NtOpenProcess:
TP делает hook на NtOpenProcess, в результате чего мы должны перехватить вызов и, в зависимости от того, кто вызывает, выполнить нужную логику:
Hook на NtOpenThread:
Логика полностью идентична NtOpenProcess. Код:
Обработка KiAttachProcess:
Функция не экспортируется, поэтому её адрес ищется через KeAttachProcess. По сигнатуре и шаблону байт находится адрес и производится восстановление:
Восстановление NtReadVirtualMemory, NtWriteVirtualMemory, DbgkpSetProcessDebugObject, DbgkpQueueMessage:
Обнуление DebugPort и контроль:
Самая интересная часть — очистка DebugPort в структуре EPROCESS, поскольку это основной объект отладки. Нужно установить аппаратную точку останова по доступу к этому полю (например, ba r4 адрес) и отслеживать обращения. Было обнаружено 4 места обнуления и 2 проверки.
Чтобы найти эти участки, нужно представить себя разработчиком игры: если игра хочет проверить DebugPort, она обязательно будет обращаться к этому адресу. Мы ставим точку останова, наблюдаем за срабатываниями и анализируем дизассемблированный код. Некоторые участки могут быть защищены виртуальной машиной (VM), и вмешиваться в них опасно.
Пример:
Первая точка:
Функция начинается с:
Анализ показывает, что это стандартное обнуление — просто возвращаемся из функции. Проверки нет
~ Обнуление DebugPort
~ NtOpenProcess
~ NtOpenThread
~ KiAttachProcess
~ NtReadVirtualMemory
~ NtWriteVirtualMemory
Следующие две функции используются для обмена сообщениями между процессом и отладчиком. Если они не обрабатываются, отладчик не сможет получать или передавать информацию:
~ DbgkpSetProcessDebugObject
~ DbgkpQueueMessage
Эти функции уже многократно описаны в интернете, поэтому здесь я лишь кратко остановлюсь на них
На мой взгляд, обнуление DebugPort — один из самых сложных моментов в защите TP, при этом в сети об этом почти нет подробностей, а имеющиеся объяснения неполные
Начнём с двухмашинной отладки:
TP постоянно вызывает KdDisableDebugger, чтобы отключить отладку. Мы просто заставляем этот вызов сразу возвращаться, так как в начале функции нет проверок. По исходному коду WRK видно, что функция также обрабатывает связанные переменные и состояния отладки — их тоже нужно корректно подменить
При первом запуске TP отладчик останавливается на:
Код:
TesSafe+0x56d5: меняем на 0x74 (NOP — 2 байта 0x90)
b13c76d5 75b0 -> jne -> 0x74
Код:
TesSafe+0x5803: меняем на 0xeb
b13c7803 7402 -> je -> 0xeb
При втором запуске TP:
Код:
TesSafe+0x59dd: меняем на 0x9090
b10419dd 75b0 -> jne -> NOP
Код:
TesSafe+0x5b0b: меняем на 0xeb
b1041b0b 7402 -> je -> 0xeb
Таким образом, TP запускается дважды: первый запуск — ложный
Hook на NtOpenProcess:
TP делает hook на NtOpenProcess, в результате чего мы должны перехватить вызов и, в зависимости от того, кто вызывает, выполнить нужную логику:
C++:
__declspec( naked ) void FuckNtOpenProcess( )
{
__asm {
pushad
pushfd
call isGameAccess
mov g_isGameAccess, al
popfd
popad
cmp g_isGameAccess, 1
jnz NOISGAME
push dword ptr[ebp - 38h]
push dword ptr[ebp - 24h]
mov eax, g_uNtOpenProcessHookAddr
add eax, 6
jmp eax
NOISGAME :
push dword ptr[ebp - 38h]
push dword ptr[ebp - 24h]
mov eax, g_uObOpenObjectByPointerAddr
call eax
push g_uHookNtOpenProcessRet
ret
}
}
Hook на NtOpenThread:
Логика полностью идентична NtOpenProcess. Код:
C++:
__declspec( naked ) void FuckNtOpenThread( )
{
__asm {
pushad
pushfd
call isGameAccess
mov g_isGameAccess, al
popfd
popad
cmp g_isGameAccess, 1
jnz NOISGAME
push dword ptr[ebp - 34h]
push dword ptr[ebp - 20h]
mov eax, g_uNtOpenThreadHookAddr
add eax, 6
jmp eax
NOISGAME :
push dword ptr[ebp - 34h]
push dword ptr[ebp - 20h]
mov eax, g_uObOpenObjectByPointerAddr
call eax
push g_uHookNtOpenThreadRet
ret
}
}
Обработка KiAttachProcess:
Функция не экспортируется, поэтому её адрес ищется через KeAttachProcess. По сигнатуре и шаблону байт находится адрес и производится восстановление:
C++:
VOID My_HookKiAttachProcess( )
{
BYTE bJmpAddr[7] = { 0x8b, 0xff, 0x55, 0x8b, 0xec, 0x53, 0x8b };
BYTE* bKeAttachProcessAddr = ( BYTE* ) GetFunAddress( L"KeAttachProcess" );
if ( bKeAttachProcessAddr == NULL ) return;
// Поиск call nt!KiAttachProcess
while ( TRUE )
{
if ( *( bKeAttachProcessAddr ) == 0xE8 &&
*( bKeAttachProcessAddr + 5 ) == 0x5F &&
*( bKeAttachProcessAddr + 6 ) == 0x5E &&
*( bKeAttachProcessAddr + 7 ) == 0x5D &&
*( bKeAttachProcessAddr + 8 ) == 0xC2 &&
*( bKeAttachProcessAddr - 1 ) == 0x56 &&
*( bKeAttachProcessAddr - 2 ) == 0x57 &&
*( bKeAttachProcessAddr - 5 ) == 0xFF &&
*( bKeAttachProcessAddr - 6 ) == 0x50 )
{
g_uKiAttachProcessAddr = *( ULONG* ) ( bKeAttachProcessAddr + 1 ) + ( ULONG ) ( bKeAttachProcessAddr + 5 );
break;
}
bKeAttachProcessAddr++;
}
KIRQL klrql = KeRaiseIrqlToDpcLevel( );
CleanPageProtect( TRUE );
RtlCopyMemory( ( BYTE* ) g_uKiAttachProcessAddr, bJmpAddr, 7 );
KeLowerIrql( klrql );
CleanPageProtect( FALSE );
}
Восстановление NtReadVirtualMemory, NtWriteVirtualMemory, DbgkpSetProcessDebugObject, DbgkpQueueMessage:
C++:
VOID My_HookNtReadAndNtWriteVirtualMemory( )
{
BYTE bReadAndWritePush[2] = { 0x6a, 0x1c };
BYTE bReadPush[5] = { 0x68, 0xe0, 0xa4, 0x4d, 0x80 };
BYTE bWritePush[5] = { 0x68, 0xf8, 0xa4, 0x4d, 0x80 };
BYTE* uNtReadVirtualMemoryAddr = ( BYTE* ) GetSsdtFunAddress( 0xBA );
BYTE* uNtWriteVirtualMemoryAddr = ( BYTE* ) GetSsdtFunAddress( 0x115 );
if ( !uNtReadVirtualMemoryAddr || !uNtWriteVirtualMemoryAddr ) return;
KIRQL klrql = KeRaiseIrqlToDpcLevel( );
CleanPageProtect( TRUE );
RtlCopyMemory( uNtReadVirtualMemoryAddr, bReadAndWritePush, 2 );
RtlCopyMemory( uNtReadVirtualMemoryAddr + 2, bReadPush, 5 );
RtlCopyMemory( uNtWriteVirtualMemoryAddr, bReadAndWritePush, 2 );
RtlCopyMemory( uNtWriteVirtualMemoryAddr + 2, bWritePush, 5 );
KeLowerIrql( klrql );
CleanPageProtect( FALSE );
}
Обнуление DebugPort и контроль:
Самая интересная часть — очистка DebugPort в структуре EPROCESS, поскольку это основной объект отладки. Нужно установить аппаратную точку останова по доступу к этому полю (например, ba r4 адрес) и отслеживать обращения. Было обнаружено 4 места обнуления и 2 проверки.
Чтобы найти эти участки, нужно представить себя разработчиком игры: если игра хочет проверить DebugPort, она обязательно будет обращаться к этому адресу. Мы ставим точку останова, наблюдаем за срабатываниями и анализируем дизассемблированный код. Некоторые участки могут быть защищены виртуальной машиной (VM), и вмешиваться в них опасно.
Пример:
Первая точка:
Код:
TesSafe+0x219e:
mov ecx, dword ptr [ecx]
Функция начинается с:
Код:
TesSafe+0x2124:
mov edi, edi
Анализ показывает, что это стандартное обнуление — просто возвращаемся из функции. Проверки нет