Этого можно добиться с помощью сканирования подписи кода, что я делал в прошлом.Концепция в основном работает, полагаясь на тот факт, что функции часто не меняются слишком сильно между обновлениями, а просто перемещаются, потому что они были перенесены вперед или назад при расширении или сжатии других функций.
Давайте рассмотрим пример MessageBoxA
, чья разборка выглядит для меня так:
765DEA11 > 8BFF MOV EDI,EDI
765DEA13 55 PUSH EBP
765DEA14 8BEC MOV EBP,ESP
765DEA16 833D 749A5E76 00 CMP DWORD PTR DS:[765E9A74],0
765DEA1D 74 24 JE SHORT USER32.765DEA43
765DEA1F 64:A1 18000000 MOV EAX,DWORD PTR FS:[18]
765DEA25 6A 00 PUSH 0
765DEA27 FF70 24 PUSH DWORD PTR DS:[EAX+24]
765DEA2A 68 A49E5E76 PUSH USER32.765E9EA4
765DEA2F FF15 34145876 CALL DWORD PTR DS:[<&KERNEL32.Interlocke>; kernel32.InterlockedCompareExchange
765DEA35 85C0 TEST EAX,EAX
765DEA37 75 0A JNZ SHORT USER32.765DEA43
765DEA39 C705 A09E5E76 01>MOV DWORD PTR DS:[765E9EA0],1
765DEA43 6A 00 PUSH 0
765DEA45 FF75 14 PUSH DWORD PTR SS:[EBP+14]
765DEA48 FF75 10 PUSH DWORD PTR SS:[EBP+10]
765DEA4B FF75 0C PUSH DWORD PTR SS:[EBP+C]
765DEA4E FF75 08 PUSH DWORD PTR SS:[EBP+8]
765DEA51 E8 73FFFFFF CALL USER32.MessageBoxExA
765DEA56 5D POP EBP
765DEA57 C2 1000 RETN 10
Хитрость заключается в том, чтобы угадать какой-то блок кода, который, по вашему мнению, останется неизменным в обновлении, но, что более важно, уникален для этогофункция.Как правило, бесполезно сканировать эпилог / пролог.Я бы, вероятно, взял следующий блок:
765DEA16 833D 749A5E76 00 CMP DWORD PTR DS:[765E9A74],0
765DEA1D 74 24 JE SHORT USER32.765DEA43
765DEA1F 64:A1 18000000 MOV EAX,DWORD PTR FS:[18]
765DEA25 6A 00 PUSH 0
765DEA27 FF70 24 PUSH DWORD PTR DS:[EAX+24]
765DEA2A 68 A49E5E76 PUSH USER32.765E9EA4
765DEA2F FF15 34145876 CALL DWORD PTR DS:[<&KERNEL32.Interlocke>;
Вы должны сделать баланс при выборе длины блока.Чем длиннее блок, тем более вероятно, что он уникально идентифицирует функцию, но также более вероятно, что какой-то код будет вставлен во время обновления, что означает, что он разделен и т. Д. Обратите внимание, что у выбранного мной блока есть несколько блоков памяти.Рекомендации.Мы не можем полагаться на какие-либо данные или адреса функций, поскольку они могут быть перемещены при следующем обновлении, поэтому мы заполняем эти байты подстановочными знаками:
765DEA16 833D XXXXXXXX 00 CMP DWORD PTR DS:[XXXXXXXX],0
765DEA1D 74 XX JE SHORT XXXXXXXX
765DEA1F 64:A1 18000000 MOV EAX,DWORD PTR FS:[18]
765DEA25 6A 00 PUSH 0
765DEA27 FF70 24 PUSH DWORD PTR DS:[EAX+24]
765DEA2A 68 XXXXXXXX PUSH XXXXXXXX
765DEA2F FF15 XXXXXXXX CALL DWORD PTR DS:[XXXXXXXX]
Это означает, что наша байтовая подпись теперь:
0x83 0x3D 0x?0x?0x?0x?0x74 0x?0x64 0xA1 0x18 0x00 0x00 0x00 0x6A 0x00 0xFF 0x70 0x24 0x68 0x?0x?0x?0x?0xFF 0x15 0x?0x?0x?0x?
Байты 0x?
указывают на символы подстановки, которые мы ожидаем изменить.Другие байты, которые мы ожидаем, не изменятся в обновлении.Чтобы использовать байты для определения местоположения функции во время выполнения, вам необходимо выполнить поиск этих байтов (с учетом подстановочных знаков).Процесс примерно такой:
- Перечисление всех исполняемых страниц процесса (
VirtualQueryEx
) - Сканирование на найденную нами сигнатуру байта (с учетом подстановочных знаков - это тривиальнореализовать в виде цикла
for
, который пропускает подстановочные байты) - Чтобы получить истинный адрес функции, исправьте полученный адрес со смещением блока из исходной функции (в данном случае
0x765DEA16 - 0x765DEA11 => 0x5
)
На самом деле, вместо перечисления всех исполняемых страниц, часто достаточно найти модуль, в котором находится функция (в данном случае (1033 *)), и выполнить поиск только в этом модуле.