Пишем исследовательский стенд
Первое, что нам нужно — это получить список всех экспортируемых функций из настоящей dll.
Сделаем это следующим кодом:
1. program GetFuncsDll; 2. {$APPTYPE CONSOLE} 3. uses Windows; 4. var 5. ImageBase: DWORD; //адрес образа dll 6. pNtHeaders: PImageNtHeaders; // PE заголовок dll 7. IED: PImageExportDirectory; // адрес таблицы экспорта 8. ExportAddr: TImageDataDirectory; // таблица экспорта 9. I: DWORD; // переменная для цикла 10. NamesCursor: PDWORD; // указатель на адрес имени функции 11. OrdinalCursor: PWORD; // указатель на адрес номера функции 12. LIB_NAME:AnsiString; // имя dll 13. BEGIN 14. LIB_NAME:='MiniLib.dll'; 15. loadlibraryA(PAnsiChar(LIB_NAME)); 16. ImageBase := GetModuleHandleA(PAnsiChar(LIB_NAME)); 17. pNtHeaders := Pointer(ImageBase + DWORD(PImageDosHeader(ImageBase)^._lfanew)); 18. ExportAddr := pNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 19. IED := PImageExportDirectory(ImageBase+ExportAddr.VirtualAddress); 20. NamesCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNames)); 21. OrdinalCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNameOrdinals)); 22. For I:=0 to Integer(IED^.NumberOfNames-1) do begin 23. WriteLn(output,PAnsiChar(ImageBase + PDWORD(NamesCursor)^),'=',OrdinalCursor^ + IED^.Base); 24. Inc(NamesCursor); 25. Inc(OrdinalCursor); 26. end; 27. Readln; 28. end. Листинг 1
Здесь трудностей вроде нет. Добираемся последовательно до таблицы экспорта (строка 19) указателей на массив имен(NamesCursor) и массива номеров(OrdinalCursor) и читаем функцию за функцией, имена и номера. Количество функций находится в поле NumberOfNames. Этот код был добыт на просторах интернета, потом доработан и упрощён.
Рассмотрим нашу тестовую dll.
1. Library MiniLib; 2. function myAdd(a,b:integer): integer; stdcall; 3. begin 4. result:=a+b; 5. end; 6. function mySub(a,b:integer): integer; stdcall; 7. begin 8. result:=a-b; 9. end; 10. exports 11. myAdd, 12. mySub; 13. begin 14. end. Листинг 2
Здесь трудностей тоже вроде нет. Экспортируем две функции — сложения и вычитания.
список экспортируемых функций и номеров у нас будет такой:
myAdd=2
mySub=1
Листинг 3
Такие номера присвоил компилятор. Почему именно такие? Этого я не знаю.
Теперь сосредоточимся на функции сложения. Посмотрим в какой код откомпилировался её вызов, для этого вызовем её и посмотрим в отладчике.
1. program TestCall; 2. {$APPTYPE CONSOLE} 3. uses Windows; 4. var 5. myAdd: function (a,b:integer): integer; stdcall; 6. Handle:HMODULE; 7. N:Integer; 8. begin 9. Handle := loadlibrary('MiniLib.dll'); 10. @myAdd := GetProcAddress(Handle, 'myAdd'); 11. //пример получения адреса функции по индексу 12. //@myAdd := GetProcAddress(Handle, PChar(2)); 13. N:=myAdd(1,2); 14. writeLn(N); 15. readln; 16. end. Листинг 4
Тут всё просто. Получаем адрес функции и вызываем её. Поясню лишь, что во втором параметре GetProcAddress указатель на имя функции, но это в том случае, если он больше $FFFF, если меньше или равен, то он воспринимается как номер функции в таблице экспорта. То есть мы можем вызвать функцию по номеру или по имени.
Теперь посмотрим как происходит занесение результата сложения в переменную, а именно работа строки 13.
1. TestCall.dpr.13: N:=myAdd(1,2);
2. push $02
3. push $01
4. call dword ptr [$0040cba4]
5. mov [$0040cbac],eax
Листинг 5
И тут всё просто, помещаем в стек, двойку(2) и единицу(3), вызываем нашу функцию (4), результат сложения помещен компилятором в регистр еах, и потом из регистра копируем результат в переменную N(5).
Вот он перед вами распространенный вызов функции из Dll. Аргументы помещаются в стек, делается call, и из регистров (или стека) считываются результаты.
Идея
Моя идея заключается в том, что когда вместо настоящей лежит моя фейковая dll, то сначала она перехватывает входы функции и имя функции, потом вызывает настоящую функцию и как будто бы ничего не было.
Пишем фейковую Dll.
Итак список функций и номеров у нас есть, но каждой экспортируемой функции должен соответствовать какой-то код. Какой. Вот ради этого всё и пишется. Те примеры, которые я видел на просторах интернета, в них полезный код для каждой перехватываемой функции клонируется, и причем еще надо знать параметры экспорта функции, чтобы вызвать настоящую с теми же самыми параметрами. Мне стало лень проводить такую кропотливую работу(по поиску описания всех функций GDS32 и дублирования на делфи) это раз. И все-таки клонировать полезный код — это «не наш метод». Идея в следующем — мы хотим, чтобы после вызова функции приложением отработал наш код. Раз код один и тот же ну вот и сделаем отдельную процедуру с полезным кодом — ProxyProc. А каждая фейковая процедура должна будет просто вызвать ProxyProc. Дальше прокси-процедура должна как-то узнать какая именно процедура вызвала её. После раздумий пришел к выводу, что идеальный вариант — это поместить в стек номер функции. Также нам надо сохранить состояние регистров и флагов, потому что они могут влиять на выполнение процедуры в настоящей DLL. Итого получаем на каждую экспортируемую функцию четыре строчки кода. И да, раз мы вмешиваемся в глубинные механизмы работы Windows, дабы быть уверенными чего и где мы запортили, писать будем на ассемблере.
1. pushfd //одно и то же для каждой функции
2. pushad //одно и то же для каждой функции
3. push 2 // меняется номер для каждой функции
4. call ProxyProc // одно и то же для каждой функции
Листинг 6
Реализуем идею
А вот и код.
1. Library minilib2; 2. 3. Uses Windows; 4. 5. Procedure ProxyProc; assembler; 6. asm 7. end; 8. 9. Procedure FakeProc0001; assembler; 10. asm 11. pushfd 12. pushad 13. push 000000001 14. call ProxyProc 15. end; 16. 17. Procedure FakeProc0002; assembler; 18. asm 19. pushfd 20. pushad 21. push 000000002 22. call ProxyProc 23. end; 24. 25. Exports 26. FakeProc0001 index 1 name 'mySub', 27. FakeProc0002 index 2 name 'myAdd'; 28. Begin 29. End. Листинг 7
Тут всё просто. Экспортируем две фейковые процедуры, а имена и номера им даем такие же как в настоящей dll.
Дальше самая хитрая часть — это сама прокси-процедура. Из чего она должна состоять.
1. Выполнить какие-то полезные нам операции с номером функции и входными параметрами
2. Узнать адрес настоящей функции
3. Вернуть все регистры к исходному состоянию
4. Передать управление на адрес настоящей процедуры, как будто бы ничего не было.
Соответственно её код может быть следующим.
1. const LibName:pAnsiChar = 'MiniLib_.DLL'#0; 2. Procedure DeveloperProc; 3. // процедура разработчика 4. begin 5. end; 6. Procedure ProxyProc; assembler; 7. asm 8. call DeveloperProc; // Взываем процедуру, в которой читаем в стеке // и регистрах, всё что хотели перехватить 9. add esp,4 // убираем адрес возврата в фейковую функцию 10. push LibName // помещаем адрес имени истинной dll 11. call LoadLibraryA // загружаем dll в память, узнаем адрес 12. push eax // помещаем этот адрес в стек 13. call GetProcAddress // номер функции же уже в стеке. узнаем адрес функции 14. mov [esp-4], eax // отмечаем в стеке этот адрес, // хотя моя версия винды уже его там отметила 15. popad // восстанавливаем регистры 16. popfd // восстанавливаем флаги 17. jmp [esp-40] // сделали свое грязное дело, // регистры и стек вернули к исходному состоянию // передаем управление настоящей функции 18. end; Листинг 8
Теперь когда мы откомпилируем этот код, то получим «minilib2.dll’. Переименуем его на „minilib.dll“ и подменим, а „minilib.dll“ переименуем соответственно в „minilib_.dll“
Теперь посмотрим как это работает
TestCall.dpr.13: N:=myAdd(1,2);
1. push $02
2. push $01
3. call dword ptr [$0040cba4] // вызываем myAdd, но попадаем в фейк
4. mov [$0040cbac],eax
Листинг 9
В листинге 9 часть уже виденного кода, который вызывает функцию из Dll и в таблице ниже состояние стека и регистров после попадания в фейковую процедуру, то есть после вхождения в call на строке 3
EAX 00364434 EBX 7FFDA000 ECX 00000000 EDX 00000003 ESI 16A1F224 EDI 13D84260 EBP 0012FFC0 ESP 0012FFA4 EIP 00364434 EFL 00000246 Листинг 10 |
0012FFAC 00000002 // второй аргумент 0012FFA8 00000001 // первый аргумент ->0012FFA4 0040811A // адрес возврата в экзешник Листинг 11 |
Дальше видим слева код нашей четырехстрочной фейковой процедуры и справа состояние стека после попадания в proxyproc, то есть после вхождения в call на строке 4
minilib2.myAdd: // она же fakeProc0002 1. pushfd 2. pushad 3. push $02 4. call $00364408 // вызываем proxyProc Листинг 12 |
0012FFAC 00000002 // второй аргумент 0012FFA8 00000001 // первый аргумент 0012FFA4 0040811A // адрес возврата в экзешник 0012FFAO 00000346 // регистр флага 0012FF9C 00364434 // регистр ЕАХ 0012FF98 00000000 // регистр ЕСХ 0012FF94 00000003 // регистр EDX 0012FF90 7FFDA000 // регистр EBX 0012FF8C 0012FFAO // регистр ESP 0012FF88 0012FFC0 // регистр EBP 0012FF84 16A1F224 // регистр ESI 0012FF80 13D84260 // регистр EDI 0012FF7C 00000002 // номер функции (02) ->0012FF78 0036443D // адрес возврата в фейковую процедуру fakeProc0002 Листинг 13 |
Дальше видим слева код прокси-процедуры и справа состояние стека после получения адреса истинной процедуры после выполнения строки 6. Видим что из стека убран адрес возврата в фейковую процедуру fakeProc0002 и убран номер функции из стека, зато в стеке появился адрес настоящей функции.
minilib2.ProxyProc: 1. add esp,$04 2. push dword ptr [$0036782c] 3. call $00364394 // это LoadLibrary 4. push eax 5. call $00364384 // это GetProcAdress 6. mov [esp-$04],eax 7. popad 8. popfd 9. jmp dword ptr [esp-$28] Листинг 14 |
0012FFAC 00000002 // второй аргумент 0012FFA8 00000001 // первый аргумент 0012FFA4 0040811A // адрес возврата в экзешник 0012FFAO 00000346 // регистр флага 0012FF9C 00364434 // регистр ЕАХ 0012FF98 00000000 // регистр ЕСХ 0012FF94 00000003 // регистр EDX 0012FF90 7FFDA000 // регистр EBX 0012FF8C 0012FFAO // регистр ESP 0012FF88 0012FFC0 // регистр EBP 0012FF84 16A1F224 // регистр ESI ->0012FF80 13D84260 // регистр EDI 0012FF7C 0037437C // адрес настоящей процедуры в настоящей dll Листинг 15 |
Дальше видим в таблице слева состояние регистров и справа состояние стека перед jmp на истинную процедуру, то есть перед тем как выполнить строку 9 листинга 14. Как видим состояние стека и регистров идентично состоянию сразу после вхождению в фейковую процедуру(листинги 10 и 11), и надеемся истинная процедура DLL не почувствует разницы. (28 в шестнадцатеричной — это 40 в десятичной, то есть 10 раз по 4 байта это как раз то место в стеке, где у нас лежит адрес истинной процедуры (листинг 17)).
EAX 00364434 EBX 7FFDA000 ECX 00000000 EDX 00000003 ESI 16A1F224 EDI 13D84260 EBP 0012FFC0 ESP 0012FFA4 EIP 00364422 EFL 00000246 Листинг 16 |
0012FFAC 00000002 // второй аргумент 0012FFA8 00000001 // первый аргумент ->0012FFA4 0040811A // адрес возврата в экзешник 1. 0012FFAO 00000346 // регистр флага 2. 0012FF9C 00364434 // регистр ЕАХ 3. 0012FF98 00000000 // регистр ЕСХ 4. 0012FF94 00000003 // регистр EDX 5. 0012FF90 7FFDA000 // регистр EBX 6. 0012FF8C 0012FFAO // регистр ESP 7. 0012FF88 0012FFC0 // регистр EBP 8. 0012FF84 16A1F224 // регистр ESI 9. 0012FF80 13D84260 // регистр EDI 10. 0012FF7C 0037437C // адрес настоящей процедуры в настоящей dll Листинг 17 |
И наконец процедура разработчика.
В этой процедуре уже не обязательно писать на ассемблере. Здесь мы собственно и можем сделать перехват, без вреда содержимому регистров и стека.
Например простым кодом, чтобы выводить в файл все номера вызываемых функций может быть такой.
1. Procedure DeveloperProc; 2. var 3. F:text; 4. _ebp:PAnsiChar; //указатель на стек 5.begin 6. asm 7. mov _ebp,ebp; 8. end; 9. assignfile(F,'G:\Projects\dllproxy\logdll.txt'); 10. append(F); 11. writeln(F,DateTimeToStr(now),': ',PDWORD(_ebp+3*4)^); 12. closefile(F); 13.end; Листинг 18
На строке 7 в переменную _ebp занесли указатель базы
на строке 9 связали переменную F с файлом
на строке 10 открыли файл для добавления
На строке 11 записали текущие дату и время, и номер вызванной функции
К указателю базы мы должны прибавить три раза по 4 байта, потому что в стеке после номера функции лежат три указателя: 1. Указатель на возврат в фейковую процедуру, 2. Указатель на возврат в прокси-процедуру и 3. Помещенный компилятором указатель на стек(push ebp). Тип указателя PAnsiChar был выбран, потому что к нему допускаются операции сложения и вычитания с числами.
На строке 12 закрыли файл.
Примеры качать здесь.
P.S. Прокси-GDS32.Dll удачно скомпилировалась, программа её использующая никаких ошибок в работе не выдала, все вызовы были перехвачены в лог-файл, неудачные sql-запросы пойманы и оптимизированы.
P.P.S. Автор данной статьи не несет ответственности за использование информации и материала в этой статье. Вся информация дана исключительно в образовательных целях.
ссылка на оригинал статьи http://habrahabr.ru/post/215389/
Добавить комментарий