Понадобилось мне перехватывать вызовы GDS32.DLL. Решил написать прокси-dll.
Пишем исследовательский стенд
Первое, что нам нужно — это получить список всех экспортируемых функций из настоящей 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:Projectsdllproxylogdll.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. Автор данной статьи не несет ответственности за использование информации и материала в этой статье. Вся информация дана исключительно в образовательных целях.
Автор: imrimat