суббота, 5 декабря 2015 г.

Fun with crackme

Захотелось поломать какой-нибудь сложный крякми, выбор пал на https://exelab.ru/f/index.php?action=vthread&forum=2&topic=22109&page=0
Смысл в том, чтобы извлечь картинку из памяти.

== Цель ==

Максимально быстро решить crackme, сведя к минимуму анализ и отладку.

== Первичный анализ ==

Статический анализ: код написан на asm, в импорте только ZwRaiseHardError, среди строк интересна лишь строка Db.dll.
Статикой информацию получить можно, но это долго, а значит не соответствует цели.
Динамический анализ: отладчик пока отложим в сторону, соберем полную картину действий crackme:

Собираем лог сисколов либо тулзой PIN, либо иным инструментом ( я использовал свой ).
В логе видны манипуляции с mapping'ом и unmapping'ом секций, а также пара интересных сисколов: NtResetWriteWatch и NtGetWriteWatch.
Кроме этого есть 3 вызова NtAllocateVirtualMemory с MEM_WRITE_WATCH, и один с MEM_COMMIT.
Далее, в логе видим 1868207 вызовов NtProtectVirtualMemory, скорее всего цикл связан с расшифровкой изображения.

Где может скрываться изображение? В выделенной приватной памяти, в промаппированной памяти или в стеке(экзотика, но почему нет?).

Проверим вариант с секцией, тем более, что в crackme есть строка "Db.dll" намекающая на маппинг dll с картинкой в ресурсах.
В логе есть 8 вызовов NtMapViewOfSection, собираем адреса, куда замаплена секция. Причем два из них находятся среди 1.8 миллионов вызовов NtProtectVirtualMemory:

Первый вызов завершается unmapping'ом:

...
NtProtectVirtualMemory
NtCreateSection
NtMapViewOfSection
NtQuerySystemInformation
NtUnmapViewOfSection
NtProtectVirtualMemory
...

А второй нет:

...
NtProtectVirtualMemory
NtMapViewOfSection
NtProtectVirtualMemory
...

Логично предположить, что картинка скрывается в этой замапленной секции, запоминаем адрес, по которому она была замаплена - 0x10000000.

Далее, перематываем 1.8 миллиона NtProtectVirtualMemory в логе, из стектрейса берем адрес, из которого был вызван сискол: 0x412B6E.

Дальше нам понадобится отладчик, но не OllyDbg, т.к. бряк на адресе 0x412B6E не работает из-за защиты от модификации секции.
Тем не менее, все работает в windbg(хотя я использовал оба отладчика). Брякаемся, ищем в отладчике конец цикла с NtProtectVirtualMemory.

Конец цикла тут:

001b:00412ee6 6681390f31      cmp     word ptr [ecx],310Fh
001b:00412eeb 7517            jne     00412f04                           [br=1]
001b:00412eed 8b4a18          mov     ecx,dword ptr [edx+18h]

Ставим бряк на 0x00412eed. Ждем пока сработает бряк, смотрим ecx:

kd> r ecx
ecx=1000101a

И видим адрес нашей промапленной секции.
Если цикл с NtProtectVirtualMemory был завязан на расшифровку картинки в ресурсах промапленной dll, то сигнатуру картинки можно будет визуально найти в памяти.
Смотрим память по адресу 0x10000000, видим сигнатуру PE файла, листаем, ничего похожего на сигнатуру картинки не видно.
Либо она где-нибудь в другом месте, либо предположение с расшифровкой неверно и она лежит тут, но зашифрована.

Отпускаем выполнение, отладчик опять брякается на 0x00412eed, и содержимое секции изменяется, после нескольких бряков по адресу 0x10002000 видна сигнатура PNG формата.

DR регистры используются при расшифровке, поэтому бряк на запись последнего байта зашифрованных данных не сработает.
Тем не менее, при срабатывании бряка в edi находится адрес, по которому будет записан dword с расшифрованными данными. Поэтому просто пишем условный бряк:

bp 412eed "j @edi = 0x1001539c '';'gc'"

Ждем срабатывание брекпойнта и дампим картинку:

.writemem f:\result.png 0x10002000 L014000

Результат:



Задача решена, причем с минимумом временных затрат, за счет использования инструментов, против которых защита crackme была бесполезна.
Что же касается самой защиты кода от модификации в данном crackme - возможно, если будет время и желание я поресерчу это в следующей статье.



вторник, 17 февраля 2015 г.

Анализ бага win32k (DOS)

На форуме rsdn.ru проскользнула любопытная тема http://rsdn.ru/forum/winapi/5956603,  в которой kero открыл баг в ядре графической подсистемы windows - win32k.sys.

Мне стало любопытно, а в чем собственно состоит баг? Данная заметка - мой небольшой анализ.

Прежде всего, как вызвать BSOD? kero, помимо exe, приложил еще и исходники, из них следует, что если создать определенное количество окон и установить каждому предыдущему окну владельцом текущее окно через SetWindowLong( GWL_HWNDPARENT ) и затем удалить самое последнее окно, то это вызовет удаление всех окон в цепочке и BSOD в итоге.

Из-за чего происходит BSOD? Из-за переполнения стека, которую вызвала рекурсия.

Рекурсия вызывается из-за:

xxxDestroyWindow()
{
   если у окна нет свойства WS_CHILD, вызывается xxxDW_DestroyOwnedWindows
}

xxxDW_DestroyOwnedWindows
{
   для всех окон, имеющих владельца родителя вызывается xxxDestroyWindow
}

Собственно, данная рекурсия не отличается от любой другой - если нет защиты от рекурсии, она рано или поздно исчерпает весь стек. Также происходит и тут, локальные переменные данных двух функций выжирают весь ядерный стек, при очередном push'е стека не хватает, происходит исключение, которое опять таки не может быть обработано, ибо также использует стековые переменные, система переключается на panic stack и выбрасывает исключение DOUBLE_FAULT.

Количество окон, которые система может создать без BSOD'a зависит от путей исполнения программы, а пути зависят от входных данных, к примеру добавив новый стиль в CreateWindow, или убрав любой из примера - получим другое число созданных окон без падения.

Почему нет функции, которая бы проверяла и предотвращала подобные вещи?

На самом деле она есть:

SetWindowLong( GWL_HWNDPARENT ) => ValidateOwnerDepth

BOOL ValidateOwnerDepth()
{
    UINT cDepth = 1;
    while (pwndOwner != NULL )
   {
       if ( pwndOwner == pwnd )
           return FALSE;
 
      pwndOwner = pwndOwner->spwndOwner; // всегда 0
      cDepth++;
    }
    return (cDepth <= NESTED_WINDOW_LIMIT);
}

spwndOwner всегда будет == 0, т.к. при создании окна, если отсутствует  свойство WS_CHILD и hWndParent == NULL, spwndOwner устанавливается в NULL.

Поэтому SetWindowLong будет успешно устанавливать владельца, т.к. функция валидатор всегда будет давать добро. А при удалении цепочки окон всегда будет BSOD если создано слишком много окон.

Баг воспроизводится на всех последних ОС любых разрядностей, не проверял лишь win10.
Спасибо kero за интересный кейс!

четверг, 2 января 2014 г.

Hacking games with PIN

Сегодня речь пойдет о играх, а точнее, об их взломе.

Может возникнуть вопрос, причем тут игры, ведь блог в основном относится к теме системного программирования / реверсинга / декомпиляции?
Да вот как-то после прочтения заметки в блоге HEX'а про создание игр (http://rebl0g.wordpress.com/2013/12/15/%D0%BC%D0%B5%D1%87%D1%82%D1%8B-%D0%B4%D0%B5%D1%82%D1%81%D1%82%D0%B2%D0%B0/#more-217) навеяло. Но в данной заметке будет рассмотрено прямо противоположное созданию действие - взлом.

Что в основном ломают в играх? Определенно, игровые ресурсы.
Как ломают тоже в принципе давно известно: ищут / меняют значения памяти в программах типа gamehack.
Но, конечно, gamehack я описывать в своем блоге не собираюсь, вместо этого я опишу альтернативный вариант поиска нужных значений в памяти.

Тут как раз и пересекается тематика блога и игр, ибо речь пойдет про PIN.

Итак,  нам нужно найти и изменить определенные игровые ресурсы, скажем, деньги. Для этого нужно как-то найти, где в памяти, а еще лучше, прямо в бинаре лежат нужные нам данные. Зачем нам бинарь, ведь достаточно изменить значения в памяти? К примеру, мы хотим еще и пореверсить код игры, или пропатчить бинарь игры, для этого желательно иметь точку, где происходит работа с игровыми ресурсами.

Как будем искать нужные данные? Подход в чем-то схож с подходом gamehack.

Игровые деньги могут уменьшаться, скажем при покупке чего-либо, или увеличиваться, с течением времени. Логично предположить, что в конечном итоге это приведет нас к двум инструкциям sub / add.

Далее, все что нужно, это написать PIN модуль, который смотрит текущие инструкции на предмет sub /add и выводит все инструкции в лог.
После чего, запускаем игру под PIN до изменения наблюдаемых величин( игровой валюты к примеру ) и после изменения.
Наблюдаемую величину можно захардкодить в PIN модуле, чтобы отсеять лишнюю информацию из лога.

В итоге, получаем нечто вроде:

VOID SubReg( ADDRINT ea, string *disasm, ADDRINT reg1Value, ADDRINT reg2Value )
{
...
    if ( reg1Value == VALUE_TO_WATCH )
        cout << hex << ea << " " << disasm << dec << " reg1Value = " << reg1Value << " reg2Value = " << reg2Value << endl;
...
}

VOID Instruction( INS ins, VOID *v )
{
    ADDRINT ea = INS_Address( ins );

    if ( ea < gMainExe.GetModuleStartAddress() || ea > gMainExe.GetModuleEndAddress() )
        return;

    if (INS_Opcode(ins) == XED_ICLASS_SUB && INS_OperandReg(ins, 0) != REG_ESP && !INS_OperandIsImmediate(ins, 1) && INS_OperandIsReg(ins, 0))
    //if (INS_Opcode(ins) == XED_ICLASS_ADD && INS_OperandReg(ins, 0) != REG_ESP && !INS_OperandIsImmediate(ins, 1) && INS_OperandIsReg( ins, 0))
    {
        if ( INS_OperandIsReg( ins, 1 ) )
        {
            INS_InsertCall( ins, IPOINT_BEFORE, (AFUNPTR)SubReg,
                                IARG_ADDRINT, ea,
                                IARG_PTR, new string( INS_Disassemble(ins) ),
                                IARG_REG_VALUE, INS_OperandReg( ins, 0 ),
                                IARG_REG_VALUE, INS_OperandReg( ins, 1 ),
                                IARG_END );
        }   
    }
}

Далее, так как на моем компе видеокарта встроенная и игр не установлено, пришлось покопаться в древних архивах, было найдено три стареньких игры, получены логи.

Пример лога(игра WarGames):

487696 sub edx, ecx reg1Value = 3f7a reg2Value = 9c4
568428 sub edx, edx reg1Value = 3f7a reg2Value = 3f7a
568428 sub edx, edx reg1Value = 3f7a reg2Value = 3f7a
568428 sub edx, edx reg1Value = 3f7a reg2Value = 3f7a

В выделенной строке лог после покупки юнита за 2500(9c4h) игровых денег.
То есть из общего баланса (3f7a) вычитается стоимость юнита. Таким образом, мы вышли на код работы с игровой валютой, дальше можно патчить в памяти через тот же PIN, или на диске или анализировать/реверсить в IDA.

Код в IDA:

.text:00487687                 xor     ecx, ecx
.text:00487689                 mov     cx, word_7470B2[edx]
.text:00487690                 mov     edx, dword_73B65C[eax]
.text:00487696                 sub     edx, ecx
.text:00487698                 mov     eax, [ebp+var_8]
.text:0048769B                 xor     ecx, ecx
.text:0048769D                 mov     cl, [eax+5]

Такой подход оправдал себя и на двух других играх, для Orion2 искалась не sub инструкция, а add, так как доход игровой валюты там меняется с течением времени.

После всех этих манипуляций мое любопытство по поводу жизнеспособности метода с PIN было удовлетворено, и игры благополучно были отправлены обратно в архив.

воскресенье, 13 октября 2013 г.

Бесконечный цикл в Driver Entry

Увидел на wasm.ru любопытный топик:

http://wasm.ru/forum/viewtopic.php?pid=538900#p538900

Содержание:

IRQL = 0. DriverEntry.
Если сделать бесконечный цикл, то на системе с одним процессором всё жутко начинает лагать - обычные приложения только через секунд 10 реагирует на клик мыши. Почему такое происходит? Ведь у кода драйвера IRQL =  0   =>  по хорошему процессор должен отдавать равное количество времени и драйверу и пользовательскому коду(он ведь выполняется тоже на IRQL = 0).

Далее в топике идут пространные рассуждения на тему.

Мне показалось это интересным, и в данной заметке я тоже порассуждаю, есть ли разница между выполнением кода в р0 в driver entry при irql ==PASSIVE LEVEL и в р3( где как известно, весь код также выполняется на PASSIVE LEVEL ).

Разница, разумеется есть. Дело вот в чем: driver entry драйвера выполняется в workitem'e, причем управление из workitem'a не вернется до тех пор, пока не отработает driver entry.

А workitem'ы, они же системные потоки это ценный ресурс, как подсказывает нам MSDN: System worker threads are a limited resource.

По факту, в системе есть несколько очередей с рабочими потоками;

typedef enum _WORK_QUEUE_TYPE
{
    CriticalWorkQueue,
    DelayedWorkQueue,
    HyperCriticalWorkQueue,
    MaximumWorkQueue
} WORK_QUEUE_TYPE;

KQUEUE ExWorkerQueue[MaximumWorkQueue];

Каждый тип очереди воркитемов разребает определенное количество потоков.

Как выше уже писалось, точка входа драйвера обрабатывается в воркитеме, и если там бесконечный цикл - мы просто забираем у системы один из системных потоков. А для DelayedWorkQueue очереди их всего 2-3!

Так в чем проблема? Забираем и забираем, что с того?

Проблема в том, что воркитемы активно используются системой, это и обнуление страниц( C2-level Security требование ) и Wmi/Etw и аудит и errorlog и реестр и page writer и прочие.

Безусловно, наиболее критичные для системы воркитемы находятся в CriticalWorkQueue, которую обслуживает большое количество системных потоков. Однако, на быстродействие системы может повлиять и воркитемы из DelayedWorkQueue, что видимо и происходит в описанном на wasm случае.

Таким образом, на уровне планировщика действительно нет разницы между выполнением р3 и р0 кода, однако кроме планировщика на быстродействие системы влияет много факторов, и в этом плане, разница между р3 и р0 кодом есть. В р0 гораздо угробить убить систему, если нет достаточной квалификации.





пятница, 17 мая 2013 г.

2 самые обычные инструкции

Mateusz "j00ru" Jurczyk, замечательный ресерчер потрохов windows, на конфе NoSuchCon среди прочих интересных вещей показал, как можно двумя инструкциями отправить windows в BSOD:

.text:00401008 start        proc near
.text:00401008                 xor     ebp, ebp
.text:0040100A                 jmp     near ptr 8053D60Ah (KiSystemServiceAccessTeb)
.text:0040100A start        endp
Причина такого поведения кроется в обработчике #PF:

        mov     ecx, offset FLAT:KiSystemServiceAccessTeb
        cmp     [ebp].TsEip, ecx
        jne     short Kt0e07

*       mov     ecx, [ebp].TsEbp        ; (eax)->TrapFrame of SysService
        test    [ecx].TsSegCs, MODE_MASK
        jz      short Kt0e07            ; caller of SysService is k mode, we will let it bugcheck.
        mov     [ebp].TsEip, offset FLAT:kss61
        mov     eax, STATUS_ACCESS_VIOLATION
        mov     [ebp].TsEax, eax
        jmp     _KiExceptionExit

Суть в том, что код обработчика принимает TsEbp за указатель на trap frame ( чем он и является при #PF в ядре ) на строке, выделенной *.
Но в данном примере, в обработчик приходит юзермодный ebp, что и приводит к BSOD'у.

вторник, 2 апреля 2013 г.

Преобразование имен файлов в формате Universal Naming Convention (UNC)

Как можно преобразовать UNC имя в ядре, чтобы оно соответствовало юзермодному имени?

То есть, например, путь:
"\Device\LanmanRedirector\Home\Users\Гость\Desktop\test.exe"
требуется преобразовать в:
"\Home\Users\Гость\Desktop\test.exe"

Немного теории.

Все удаленные файловые операции проходят через multiple UNC provider (MUP), который перенаправляет запросы подходящему сетевому редиректору (UNC провайдеру). Перенаправление происходит через подмену имени файла и установки STATUS_REPARSE, что вынуждает менеджер ввода-вывода перепослать IRP уже с новым именем.

В новое имя входит имя провайдера, ответственного за обработку запроса. Как же MUP выбирает нужного провайдера? Логично предположить, что существует некий список провайдеров. Действительно, такой список есть в реестре, в ветке:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order]
"ProviderOrder"="RDPNP,LanmanWorkstation,webclient"

Через наличие префиксов в имени файла, или через посылку IOCTL_REDIR_QUERY_PATH всем провайдером в списке, MUP выбирает нужного провайдера.

Каждый провайдер является службой, соответственно, имеет свою запись в ветке HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services.
Например, для LanmanWorkstation можно найти соответствующее имя девайса:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\LanmanWorkstation\NetworkProvider]
"DeviceName"="\\Device\\LanmanRedirector"

Таким образом, решение задачи преобразования имени состоит в перечислении всех имен служб UNC провайдеров, и получение имени девайса.
Соот-но последним шагом будет удаление \Device\unc provider name из исходного имени файла. Захардкодить имена провайдеров нельзя, т.к. они могут добавляться вызовом ф-ции FsRtlRegisterUncProvider.

четверг, 10 января 2013 г.

Dos в Win7/8

Обнаружил занятную вещь.
Если попытаться загрузить драйвер diskdump.sys или dumpata.sys из стандартного каталога с драйверами windows, то произойдет BSOD:

SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)
This is a very common bugcheck.  Usually the exception address pinpoints
the driver/function that caused the problem.  Always note this address
as well as the link date of the driver/image that contains this address.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: 82645e85, The address that the exception occurred at
Arg3: 807df9f0, Exception Record Address
Arg4: 807df5d0, Context Record Address

Debugging Details:
------------------

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - <Unable to get error code text>
...
STACK_TEXT: 
807dfab8 965f339f 0052005c 00000000 00010000 nt!memset+0x45
807dfad8 827cf2e6 8647c2b8 86688000 00000000 Diskdump!DriverEntry+0x1b
807dfcbc 827d2d98 00000001 00000000 807dfce4 nt!IopLoadDriver+0x7ed
807dfd00 82689a6b 8a692cd0 00000000 84491640 nt!IopLoadUnloadDriver+0x70
807dfd50 82814f9e 00000001 41adab71 00000000 nt!ExpWorkerThread+0x10d
807dfd90 826bd1f9 8268995e 00000001 00000000 nt!PspSystemThreadStartup+0x9e
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x19

Смотрим код:

Diskdump!DriverEntry:
965f3384 8bff            mov     edi,edi
965f3386 55              push    ebp
965f3387 8bec            mov     ebp,esp
965f3389 53              push    ebx
965f338a 56              push    esi
965f338b 8b750c          mov     esi,dword ptr [ebp+0Ch]
965f338e 57              push    edi
965f338f bf00000100      mov     edi,10000h
965f3394 57              push    edi
965f3395 6a00            push    0
965f3397 ff7608          push    dword ptr [esi+8]
965f339a e805110000      call    Diskdump!memset (965f44a4)

Итак, падение происходит из-за обнуления памяти размером 10000h по невалидному адресу. Адрес виден в стеке: 0052005c

То, что он невалиден понятно по его виду, это юзермодный адрес, а Driver Entry выполняется в контексте системного потока. Но можно и проверить:

kd> !pte 0052005c
                    VA 0052005c
PDE at C0600010            PTE at C0002900
contains 0000000000000000
not valid

Откуда берется невалидный адрес? Он приходит из второго аргумента DriverEntry. По документации там всегда валидная копия юникодной строки, представляющей из себя registry path:
NTSTATUS DriverEntry( _In_ struct _DRIVER_OBJECT *DriverObject, _In_ PUNICODE_STRING RegistryPath )
Собственно, невалидный адрес и является частью этой юникодной строки:

5c 00 52 00 это \.R, первые 2 WCHAR от строки
\.R.E.G.I.S.T.R.Y.\.M.A.C.H.I.N.E.\.S.Y.S.T.E.M.\.C.o.n.t.r.o.l.S.e.t.0.0.1.\.s.e.r.v.i.c.e.s.\.D.i.s.k.d.u.m.p.

Теперь разберемся, почему драйвер diskdump.sys считает, что вторым агрументом придет какая-то структура, которую он должен заполнить, вместо registry path. После непродолжительных поисков находится виновник - драйвер crashdmp.sys, в который начиная с windows vista перекочевал из ядра весь код, связанный с крешдампами.

Драйвер crashdmp.sys загружает ядро в ф-ции IopLoadCrashdumpDriver(), где в качестве второго аргумента в DriverEntry приходит указатель на таблицу колбеков. К слову,  crashdmp.sys также портит память со строкой registry path, но до BSOD'а дело не доходит, по причине слишком длинной строки registry path.

Далее, в CrashdmpLoadDumpStack грузятся остальные драйвера из стека драйверов, в которые также передается вторым параметром в DriverEntry структура, которую они заполняют своими ф-циями.

Собственно, разработчики windows не учли тот факт, что драйвера из стека crash dump i/o path можно загрузить как обычный драйвер. Никаких проверок для предотвращения их загрузки нет. Поэтому и возможен такой локальный DOS.

Данного рода баг существует как минимум на win7 и win8 как х86, так и х64.

Теперь посмотрим, возможна ли тут EoP (elevation of privilege).

.text:0001239F                 push    dword ptr [esi+4Ch] ; size_t <= 00300074
.text:000123A2                 lea     ebx, [esi+0Ch]
.text:000123A5                 push    0               ; int
.text:000123A7                 push    dword ptr [ebx] ; void * <= 00470045
.text:000123A9                 call    _memset
.text:000123AE                 push    dword ptr [esi+4Ch] ; size_t <= 00300074
.text:000123B1                 push    0               ; int
.text:000123B3                 push    dword ptr [esi+10h] ; void * <= 00530049
.text:000123B6                 call    _memset
.text:000123BB                 mov     eax, [esi+8]                 <= 0052005c
.text:000123BE                 lea     ecx, [eax+10h]               <= 52006c
.text:000123C1                 mov     _DeviceExtension, ecx        <= 52006c
.text:000123C7                 and     dword ptr [eax], 0
.text:000123CA                 mov     dword ptr [eax+8], 2F0h
.text:000123D1                 mov     edx, _DeviceExtension
.text:000123D7                 lea     ecx, [eax+2F0h]
.text:000123DD                 mov     [edx+18h], ecx
.text:000123E0                 mov     ecx, edi
...
.text:000123F0                 mov     eax, _DeviceExtension
.text:000123F5                 mov     eax, [eax+18h]
.text:000123F8                 and     dword ptr [eax], 0 <======== bingo!

_DeviceExtension контролируемая атакующим структура, поле [eax+18h] контролируется, можно записать туда ядерный адрес, и он будет обнулен.

Реализация атаки через обнуление произвольного нулевого адреса давно известна, причем не только ресерчерами, но и security командой windows, которая в win8 прикрыла выделение памяти по нулевому адресу.

Однако, напомню, что дело происходит в Driver Entry, которая выполняется в контексте системного потока. То есть даже на win7 нельзя будет проэксплуатировать уязвимость с обнулением dword'а по нулевому адресу.

Кроме того, по этой же причине ( системный поток ), также не получится и просто добраться до инструкции, обнуляющей нужный адрес. Потому как для успешного выполнения нескольких ф-ций memset нужно, чтобы требуемые адреса были валидны. А они будут валидными, только если научиться в контексте системного процесса каким-либо образом выделить память. Мне таких способов не известно, боюсь, их вообще не существует. Для драйвера dumpata.sys картина примерно такая же.

Таким образом, реализация EOP невозможна, только DOS.