|
Chingachguk /HI-TECH Опубликовано: dl, 12.04.03 07:57 Что собираемся делать ?   Объект исследования: программа Ulead COOL 3D, триальная защита   Инструменты: Soft-Ice (под '98), немного мозгов   Цель: изучение win32-программ, обладающих защитой от отладки   Примечание: ничего особо нового, простая техника, SEH, etc...   Ниже я собираюсь исследовать защиту этой проги, которая позволяет пользователю использовать ее функциональность только в течении календарного месяца (30 дней) или купить возможность ее использования неограниченно во времени (вид входного окошка после инсталляции, инсталлятор UC3D35TBYB_E.exe):     Что она вообще делает ? Примерно - это какой-то графический редактор (вид после нажатия кнопки "TRY"):    Таким образом в первом приближении сразу после инсталляции данная программа позволяет полноценно работать, но в стартовом окошке напоминает о том, что до конца использования осталось 30 дней ("Remaining Days - 30") и предлагает, видимо, купить разрешение на полное использование ("BUY").  Поисследуем реакцию программы на изменение системной даты. Поменяем текущую дату на один день вперед:    Ага, программа предупреждает нас, что осталось на 1 день меньше. Если перевести дату ровно на месяц вперед, то кнопка "TRY" становится недоступна и число оставшихся дней равно 0:     Кнопка "TRY" недоступна. Попробуем поймать программу на том, как она закрывает для доступа кнопку и поисследовать, чем она при этом руководствуется. Win32-приложение обычно делает это при помощи API-функции EnableWindow:  EnableWindow(Id_Объекта_Окна,0/1-закрыть/открыть Объект)  Устанавливаем на машине дату таковой, чтобы программа решила, что осталось 0 дней для использования, устанавливаем breakpoint в SoftIce:  bpx EnableWindow   запускаем программу, получаем break и видим в отладчике место вызова (оказалось, код принадлежит некоей xsystem.dll, которую можно найти в том каталоге, куда инсталлировалась программа Ulead COOL 3D): 026D7F5F: mov edi,User32!EnableWindow push edi mov edi,esp pushad lea esi,[ebp+2C63h] lodsb cmp al,90h jnz 026D7F8A push 03E8h push dword ptr [edi+8] call [ebp+2B50h] push 0 ; FALSE - disable button "TRY" push eax call [ebp+2B54h] ; Точка вызова EnableWindow 026D7F8A: popad ...   Итак, программа анализирует первый байт некоторой структуры (lea esi,[ebp+2C63h], lodsb, cmp al,90h), сравнивает его с 90h, если он оказывается таковым, то кнопка "TRY" становится недоступной (переход на метку 026D7F8A). Попробуем проверить гипотезу о том, что все, что нужно для "освобождения" программы от проверки даты - это сделать прямо здесь достпуной кнопку "TRY". Попробуем сделать это прямо в отладчике: установим break на адрес 026D7F5F:  bpx LoadLibraryA ; Ждем загрузки xsystem.dll   ...  bc 0 ; clear breakpoint at LoadLibraryA  bpx 026D7F5F ; Set breakpoint   Ctrl+D...   и попытаемся дождаться срабатывания break'а и "руками" поменять в отладчике значение регистра al сразу после lodsb: но вместо того, чтобы получить break на этом (026D7F5F) адресе, мы получаем GPF... Видно (в Sice'е), что машина пытается исполнять какой-то мусор в xsystem.dll.   ...Так мы выяснили, что программа не только контролирует системную дату, но и сопротивляется нашим попыткам ее трейсить ;) Наверное, программа как-то динамически модифицирует свой код - например по адресу 026D7F5F - и break, который мы пытаемся установить, мы устанавливаем в то место, которое впоследствии будет изменено. Скажем, самораспаковывающийся или саморасшифровывающийся код может быть причиной тому. Попробуем как-то обойти это ограничение и вспоминаем про то, что программа неминуемо должна узнать системную дату - ведь ей необходимо ее сравнить с чем-то типа даты инсталляции. Win32-приложение обычно делает это при помощи API-функции GetSystemTime:   GetSystemTime(offset структуры типа SYSTEMTIME) SYSTEMTIME STRUCT wYear WORD ? wMonth WORD ? wDayOfWeek WORD ? wDay WORD ? wHour WORD ? wMinute WORD ? wSecond WORD ? wMilliseconds WORD ? SYSTEMTIME ENDS   Устанавливаем break на эту API-функцию:  bpx GetSystemTime   И обнаруживаем, что вызов произошел из следующего места (опять xsystem.dll): 026D6DE3: lea esi,[ebp-100h] push esi call [ebp-4] ; call GetSystemTime pop ebx 026D6DEE: movzx edx,word ptr [esi] ; d esi -> D3 07 03 00 05 00 1C ; edx= 07D3h shl edx,8 mov dl,[esi+02] ; Месяц shl edx,8 mov dl,[esi+06] ; День ; edx=07D3031C push edx   Уже видно, как программа работает с датой: код по адресу ~026D6DE3 получает косвенным call'ом дату от Win, собирает в 32 битах edx год (07D3h=2003), месяц (03) и день (1Ch=28) - 28 марта 2003 года в данном случае, делает что-то еще и управление попадает к коду в ~026D7F5F, который уже знает, в валидном ли интервале дат мы находимся и закрывает кнопку "TRY", если это не так. Но мы помним, что нужно все же попробовать подменить результат манипуляций с датой прямо перед принятием решения о disable кнопки "TRY". Ранее нам сделать это не удалось - видимо, после загрузки dll в память ее (?-пока не знаем) код самомодифицируется и мешает ставить полноценные break'и. Однако есть надежда, что в момент выполнения кода по адресу ~026D6DE3 - кода, спрашивающего дату у Win - код в ~026D7F5F расшифрован и доступен для отладки. Повторяем попытку установить break на 026D7F5F, но уже после того, как мы попали в 026D6DE3 и опять получаем GPF ! Почему, ведь код уже явно виден в отладчике ? Может, дело в том, что адрес API EnableWindow прописывается динамически прямо в команду mov edi,const_addr ? Пробуем установить break на команду загрузки регистра esi адресом структуры (lea esi,[ebp+2C63h]) и ... оказываемся в нужном месте: 026D7F5F: mov edi,User32!EnableWindow ... 026D7F68: lea esi,[ebp+2C63h] lodsb ; al = 90h если запрещенный период, иначе всегда 6Ah cmp al,90h jnz 026D7F8A push 03E8h push dword ptr [edi+8] call [ebp+2B50h] push 0 ; FALSE - disable button "TRY" push eax call [ebp+2B54h] ; Точка вызова EnableWindow 026D7F8A: popad ...   Ура !? Что ж, проверим: модифицируем (в отладчике) регистр al после лодсб и отпускаем программу...     ...Так, кнопка "TRY" доступна (хотя Remaining Days = 0). Жмем ее и видим всего лишь вот этот MessageBox:     Вот и познакомились ;) Оказывается, защиту зовут "xLok". А разрешить жать на кнопку - не значит получить доступ к функциональности ;(   Но что если данными для проверки валидности даты для кода в ~026D7F5F является не байт, а слово или даже что-то большее ? На такую мысль может навести пара команд "lea esi,[ebp+2C63h], lodsb" вместо "cmp byte ptr [ebp+2C63h],90h", хотя это может быть "приколом" компиллятора. Внимательнее поизучаем байты в структуре, адресуемой по [ebp+2C63h] для двух дат: для даты в интервале работы и вне ее: Работа возможна: 6A 01 FF 73 08 FF 95 54 ... Работа запрещена: 90 90 FF 73 08 E8 9D 00 ...   Установим дату вне интервала работы, опять остановимся в отладчике в точке 026D7F68, поменяем "неверные" байты на "верные" (90 90 FF ... -> 6A 01 FF ... ) и "отпустим" программу. MessageBox'а от xLok'а больше нет, но есть банальный GPF...   Следовательно, с большой вероятностью можно заключить, что защита выполняет (динамически) расшифровку какой-то части рабочего кода и снятие всех ее проверок после расшифровки приводит к неизбежному краху, так как этот код расшифровывается неверно вне интервала работы программы. Видимо, защита использует дату (точнее, не саму дату, а некоторую функцию от нее, которая постоянна на интервале работы программы) как ключ шифрования части своего кода. Проверим это: в этом случае будет достаточно подменить системную дату, которую защита спрашивает у Win. В отладчике это легко сделать: опять устанавливаем break на GetSystemTime, возвращаемся в xsystem.dll к коду в 026D6DEE и "патчим" значение структуры SYSTEMTIME (или значение edx чуть ниже) датой инсталляции... Все OK, программа отлично работает и ничего не запрещает.   Здорово, вроде бы осталось поменять команды инициализации edx (код в 026D6DEE и ниже) на что-то вроде таких: 026D6DEE: ; Было так: ; movzx edx,word ptr [esi] ; d esi -> D3 07 03 00 05 00 1C ; shl edx,8 ; mov dl,[esi+02] ; Месяц ; shl edx,8 ; mov dl,[esi+06] ; День ; А будет так: mov edx,Const_Valid_Date nop ... nop push edx   и все будет работать ? Пробуем найти опкоды оригинальных инструкций в DLL: и не находим их ! Этого следовало ожидать еще тогда, когда стало ясно, что код самомодифицирующийся. Что делать дальше ? Может быть, имеет смысл попробовать найти расшифровшик, проанализировать его алгоритм и "пропатчить" нужные байты в соответствии с алгоритмом ? Пусть алгоритм дешифровщика состоит в наложении xor-последовательности на байты DLL:Команды Опкод movzx edx,w [esi] 0F B7 16 shl edx,8 C1 E2 08 mov dl,[esi+2] 8A 56 02 ... Команды Опкод Шифро-Опкод в DLL (Опкод) xor (Шифро-Опкод в DLL) movzx edx,w [esi] 0F B7 16 B5 2A 1A BA 9D 0C shl edx,8 C1 E2 08 C4 45 2E 05 A7 26 ...   В этом случае не составило бы труда подготовить те "новые" опкоды, которые мы собираемся поместить на место старых - нужно выполнить над ними операцию с той же xor-последовательностью - как это делает пока неизвестный нам расшифровщик: Команды Опкод_новый (Опкод) xor (Шифро-Опкод в DLL) Результат mov edx,07D3031Ch BA 1C 03 D3 07 BA 9D 0C 05 A7 00 81 0F... nop ...   Помещаем в файл xsystem.DLL по смещению (в файле) 6DEE подготовленные таким образом байты и пытаемся смотреть, что же сделал с ними расшифровщик - а в результате видим такой вот MessageBox:     Да, защите удалось не только скрыть результат работы расшифровщика (если он вообще отработал) но и определить модификацию собственного кода. Неплохо ! Как же ему это удалось ? Попробуем "отловить" его на обращении к памяти, содержащей измененные коды:  bpm 026D6DEE   и, действительно, наблюдаем следующий код: 026D510C: push esi push ecx push eax ... 026D5115: mov ecx,49A0h xor edx,edx xor eax,eax @@GetCRC: lodsb ; esi=026D6DEFh add edx,eax dec ecx jnz @@GetCRC ... xchg edx,eax ; eax->1FBFFAh pop edx pop ecx pop esi add dword ptr [esp],5 026В515A: ret   Очень интересный код ! Похоже на подсчет CRC (возвращает в eax), причем регион подсчета CRC включает и критичные для нас адреса в ~026D6DEE. Следовательно, прежде чем воевать с их расшифровщиком, нам нужно победить подсчет контрольной суммы. Открыты ли байты подпрограммы подсчета CRC в DLL ? 026D510C: Команда / Опкод push esi / 56h push ecx / 51h push eax / 52h ...   Ищем их в файле DLL по смещению 510Ch и находим ! Следовательно, мы можем без особой боязни заменить оригинальную подпрограмму подсчета CRC на что-то вроде: @@NewCRCProc: mov eax,1FBFFAh add dword ptr [esp],5 ret @@EndOfNewCRC: db (026D515Bh-026D510Ch) - (@@EndOfNewCRC-@@NewCRCProc) dup(90h) @@NewCRCProcDone:   При этом мы вернем нужное где-то дальше CRC (в eax) без всякого подсчета - что зря процессор гонять, хитро вернемся назад (add dword ptr [esp],5) и забьем все остальное место nop'ами. Патчим таким образом DLL и получаем ... GPF ! Оба-на, защита перестала детектировать сосбтвенную модификацию но и программа "рухнула" ;( Неужели мы неправильно составили новый код подсчета CRC ?... Стоп, а ведь мы еще раньше поменяли байты, относящиеся к получению даты (в ~026D6DEE) ! Установим break на API GetSystemTime, вернемся к коду получения даты в DLL и видим, что расшифровщик явно сделал не то, что мы ожидали: Команды_новые Результат работы расшифровщика / Опкод 026D6DEE: mov edx,07D3031Ch sbb byte ptr [esi] / 1D 82 1E 1D nop mov bl,7Fh / B3 7F ...   Следовательно, алгоритм расшифровщика производит расшифровку байт в ~026D6DEE в зависимости от исходных байт. Т.е. это не простое наложение XOR-последовательности; расшифровка каждого байта зависит от того, как был расшифрованы все предидущие. Алгоритм расшифровщика нам по-прежнему неизвестен и мы не можем даже пытаться осуществлять атаку на его шифр. Откатим пока эти изменения, оставив лишь получение CRC и продолжим следить в отладчике за ходом выполнения программы защиты после возврата из подпрограммы подсчета CRC: 026D5366: sub eax,1FBfFAh ; если пришло верное CRC=1FBfFAh, то в eax будет 0 pop ebx jz 026D5382 ... 026D5382: mov [ebp+3C16h],ebx mov edx,[ebp+3C36h] push dword ptr fs:[0] mov fs:[0],esp push eax push ebp jmp 026D53A1 ... 026D53A1: pushfd invalid (0F1h) db 0BEh,0F0h,...   Если продолжить в отладчике пошагово трассировать код с 026D53A2, то налетаешь на GPF. Но сразу бросаются в глаза явно вычурные для win32-кода команды работы с сегментными регистрами (mov fs:[0],esp). Если уж приложение взялось за это, то явно не с добрыми намерениями ;) На самом деле таким образом в windows пользовательским программам дозволяется установить свой обработчик исключительных ситуаций и в случае таковых получить управление и попытаться их обработать (интересно, кому-нибудь удавалось обрабатывать их инчае как выдать сообщение "Программа выполнила ... и будет закрыта" ?...). Далее мы увидим, что иногда такой обработчик может выполнять некоторые "полезные" действия.   В двух словах о таких обработчиках. Эта штука называется "SEH" - structure exeption handler. Для того, чтобы обработчик правильно получил управление, необходимо: - поместить в стек смещение обработчика; - сохранить смещение старого обработчика на том же стеке; - записать по селектору из FS, смещению 0 содержимое esp   Сам обработчик вызвается в C-формате, при этом получая кучу параметров - указатель на структуру регистров в момент исключения и т.п.: SEHproc proc C pExcept: dword, pFrame: dword, pContext: dword, pDispatch: dword PrintException pExcept ... ret SEHproc endp   Но самое главное - это то, что он может вернуться в достаточно произвольное место кода (включая и вызвавшее исключение), при этом уведомив диспечер о том, что исключение им обработано (например, mov eax, ExceptionContinueExecution перед выходом) и то, что он имеет право записи в сегмент команд (это необходимо для исправления "дефектного" кода). Таким образом, обработчик исключения защиты явно собирается именно этим заниматься: подать управление на неверный опкод, поймать expeption, дешифровать некоторые байты, вернуться опять на неверный опкод (не обязательно первый), и так до тех пор, пока необходимый код не будет полностью расшифрован.   Где же защита разместила свой обработчик ? Нет нужды копаться в командах, предшествующих коду установки SEH (~026D5382), можно просто посмотреть стек в этот момент (скажем, после выполнения mov fs:[0],esp):  d esp   Оказыватся, адрес у обработчика равен 026D5040, а вот и он сам: 026D5040: pushad mov edx,0C8EC918Bh call 026D504B 026D504B: pop edi ; edi-> 026D504B, получение текущего смещения mov esi,36h add esi,edi ; esi=026D5081h mov ecx,94h push esi sub esi,9 mov edi,esi xor eax,eax 026D5061: lodsb xor eax,edx rol edx,5 imul edx,edx,0FB712715h add eax,0AB358CDFh stosb dec ecx jnz 026D5061 026D5078: db 0F4h,041h,0ECh,076h,03Ah... 026D5081: ...   Итак, обработчик SEH сразу после получения управления начинает что-то расшифровывать с адреса 026D5078 и ниже довольно незамысловатым алгоритмом, немного странно реализованным (вместо add eax,0AB358CDFh достаточно add al,0DFh). Размер байт для дешифровки невелик (mov ecx,94h). Дожидаемся возможности установить break в 026D5078 - после первого же stosb там появлется pop esi и после срабатывания break'а видим весь расшифрованный код: 026D5078: pop esi mov eax,[esp+28h] mov [eax+4],esi ; <-026D5081h popad 026D5081: push ebp mov ebp,esp push esi,edi,ebx,ecx,edx mov eax,[ebp+10h] mov edi,[eax+0B8h] 026D5093: mov edx,2 026D5097: jmp 026D5099 026D5099: call 026D509E 026D509E: pop ebx ; ebx<-текущее смещение 026D509E add ebx,0FFFFFFE3h ; ebx=026D5081h add edx,[eax+0B4h] mov [ebx+12h],edx ... mov ebx,0BB51B5E3h test esi,esi jz 026D50CE xor [esi],bl 026D50CE: cmp byte ptr [edi],09Dh jz 026D50DF xor [edi],bl or dword ptr [eax+0C0h],100h 026D50DF: xor ebx,ebx mov [eax+4],ebx, mov [eax+8],ebx, mov [eax+10h],ebx mov dword ptr [eax+18h],101h mov [edx],edi xor eax,eax push eax, push eax, dec eax, push eax, mov eax,ofs Kernel!FlushInstructionCashe call eax xor eax,eax pop edx,ecx,ebx,edi,esi,ebp ret ; Последняя расшифрованная команда 026D510Ch:   Что же любопытного в раскрывшемся коде ? Прежде всего обратим внимание на характерный код в 026D5081 - push ebp, mov ebp,esp. Это явно оформлено начало какой-то процедуры, и она заканчивается в 026D510B. Командами mov eax,[esp+28h] mov [eax+4],esi ; <-026D5081h   декриптор определил адрес нового обработчика SEH - теперь это будет код в 026D5081. При следующих исключениях всегда будет вызываться именно эта процедура, что-то расшифровывающая командами: xor [esi],bl 026D50CE: cmp byte ptr [edi],09Dh jz 026D50DF xor [edi],bl or dword ptr [eax+0C0h],100h 026D50DF:   Но в данный момент код получения даты (в ~026D6DEE) еще не расшифрован. Может быть, только что расшифрованная SEH-подпрограмма 026D5081 и есть ее декриптор ? Устанавливаем break на начало 026D5081:  bpx 026D5081   и наблюдаем, как раз за разом управление переходит к SEH-обработчику. Он явно расшифровывает какой-то код и хотелось бы остановить программу в тот момент, когда он полностью закончит свою работу. Довольно утомительно было бы жать Ctrl-D столько раз, поэтому я написал небольшой код, заменяющий собой распаковщик SEH-обработчика: ; Новые команды по адресу 026D5040 (первоначальный SEH-обработчик) @@NewBytes: pushad call @@GetOfsNewBytes @@GetOfsNewBytes: pop esi add esi,offset @@ArtBytes - @@GetOfsNewBytes mov eax,[esp+28h] mov [eax+4],esi popad db 0EBh ; jmp на адрес 026D5081 db (@@NewBytes+40h) - $ @@ArtBytes: pushad call @@GetOfs @@GetOfs: pop eax inc word ptr [eax+(offset CounterCalls - offset @@GetOfs)] mov ax,word ptr [eax+(offset CounterCalls - offset @@GetOfs)] popad db 0EBh ; jmp на адрес 026D5081 db (@@NewBytes+40h) - $ CounterCalls dw 0 @@DoneNewBytes: LastNops db (@@DoneOldBytes - @@OldBytes) dup (90h) ; nop only   Новый код в отличии от старого не декриптует SEH-обработчик (это
можно сделать прямо в файле xsystem.dll и не обременять код графического
редактора дешифрацией), а устанавливает новый адрес SEH-обработчика - это
будет подпрогрммка @@ArtBytes, которая только лишь вычисляет число call'ов
SEH'а и передает jmp'ом управление на старый адрес 026D5081. Теперь в Sice'е
можно ставить  bpx   Заранее, конечно, число call'ов SEH'а неизвестно, но простым
методом деления "отрезка" пополам можно довольно быстро добраться до конца
работы SEH-обработчика. Трейсим последний вызов по SEH-обработчика и видим,
что:
  - Даже когда он закончил свою работу, код получения даты (в ~026D6DEE)
нерасшифрован;
  - Последний свой xor SEH-обработчик выполнил с адресом 026D540Ch:
  Таким образом, SEH-обработчик-дешифровщик не расшифровал код
получения даты. Но что за байты появились по адресу 026D540Ch ?...
  О, старые знакомые ! :) Опять переносимый (call 026D5412, pop esi)
код дешифрует нижележащие байты. Ставим break в конец цикла, дожидаемся
его окончания и с нетерпением смотрим ... нет, не байты после 026D5440, а
байты получения даты - ~026D6DEE. Они расшифрованы, теперь - ура - мы знаем
кто их декриптует и этот кто-то (026D540C - последний дешифровщик) крайне
мал, чтобы возится с его расшифровщиком (обработчик SEH'а) - проще вбить
его, уже нам известный на то же место, где сейчас лежит его зашифрованный
собрат. Только тогда нужно будет дезактивировать его расшифровщик
(обработчик SEH'а) - а то он все испортит ;) Как же это сделать ? Вернемся
к моменту инсталляции SEH'а:
  Что будет, если вместо invalid-команды, намеренно сделанной для
генерации исключения, сделать переход на дешифровщик в 026D540C, который
теперь, как мы планируем, будет записан нами в открытом виде прямо в DLL ?
SEH-обработчик явно не вызовется, осталось опасность потерять стек при таком
прыжке. Но парная команда к pushfd - последняя команда перед вызовом
SEH-обработчика через исключение - popfd:
  как будто бы явно написана разработчиками защиты с целью восстановить
состояние процесса после отработки SEH'а. Так и поступим. Остался последний
момент: где разместить код, который поменяет пресловутые команды получения
даты на одну спокойную mov edx,Const_Valid_Date и парочку nop'ов ?
  Ниже привожу свое решение, возможно немного надуманное. Поскольку этот
код хотелось бы разместить после отработки расшифровщика 026D540C, т.е. тогда,
когда уже код от 026D5440 и далее расшифрован, то он должен быть после
последней команды расшифровывающего цикла:
  Вместо двух байт короткого jmp'а сложновато сделать такое, но если
немного пооптимизировать код расшифровщика и вспомнить, что после удаления
старого кода подсчета CRC осталась куча места, то можно написать следующее:
  Этой командой мы закроем выполнение SEH'а:
  Это код нового, открытого декриптора по адресу 026D540C:
  Это новый код подсчета CRC вместе с подпрограммой модификации
кода получения даты в 026D6DEE:
  Вот вроде бы и все ;) Хотя далее есть еще несколько пугающих
кусков типа:
  которые заставили меня лишний раз заглянуть в Ральфа Брауна, но
особой опасности они не представляют ;)
Немного извращений или граффити
  После реализации подмены системной даты и многократных смен системной
даты в целях выяснинения особенностей защиты программы можно наблюдать
интересный эффект:
 
  xsystem.dll уже изменена, все работает, но вот с числом оставшихся
дней что-то не то. Как бы не мешает, но некрасиво... Любопытен тот факт,
что обратная замена исходной DLL дела не меняет: число оставшихся дней
меняется, но как-то "криво". Есть и такой "баг": если даже взять нетронутую
версию программы, перевести дату на некоторое число дней вперед, запустить
программу, а следом вернуть дату на место, то при первом запуске программа
не позволяет работать (число оставшихся дней равно 0, кнопка "TRY" недоступна).
  Анализ кода xsystem.dll показал, что после того, как системная
дата получена программой, выполняется следующий код:
  - из некоторой области памяти достатется число, которое
"расшифровывается" xor-ом и далее (код здесь не приводится) трактуется
как дата начала работы программы (инсталляции). Поэтому для завершенности
изменений в программе можно поменять команду "xor eax,7895ADEBh"
на "mov eax,Const_Valid_Date". В этом случае программа будет выдавать
30 дней до конца trial'а. Но можно немного поизвращаться и оставить
число Remaining Days вот таким:
 
 
Исходный текст патчера
  Ниже приводится текст почти полный текст программы, выполняющей
описанные выше действия. Некоторые вспомогательные подпрограммы типа
GetCL пропущены.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
|
|