Pull to refresh

Простая прокси-DLL своими руками

Reading time 8 min
Views 16K
Понадобилось мне перехватывать вызовы 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:\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. Автор данной статьи не несет ответственности за использование информации и материала в этой статье. Вся информация дана исключительно в образовательных целях.
Tags:
Hubs:
+14
Comments 18
Comments Comments 18

Articles