Pull to refresh

Исследование защиты программы Quick Password Recovery PRO 1.7

Reading time14 min
Views3.2K
Всем привет. Сегодня я хочу на примере программы Quick Password Recovery Pro 1.7.1 показать исследование защиты в которой используются пару интересных приемов и хеш-функция.


Цель: Quick Password Recovery PRO 1.7.1 (www.passdecrypt.ru)
Защита: HWID + MD5
Инструменты: Die 0.65 + плагин KANAL 2.92, OllyDbg 1.10 (дальше Olly) + плагин mapimp (можно найти на tport.org), IDR (Interactive Delphi Reconstructor), Keygener Assistant 1.7, Borland Delphi 7 (для написания кейгена).
Начнем «осмотр пациента»:
image
Программа написана на Borland Delphi [ver: 2006] | Object Pascal, поэтому сразу грузим ее в IDR для анализа и создания map-файла для Olly.
Плагин Kanal находит несколько криптосигнатур:
image
Запускаем программу, нас встречает окошко с сообщением, что это незарегистрированная версия и некоторые функции недоступны. В этом мы можем убедиться сами:
image

image

Нажимаем кнопку «Регистрация»:
image
Для регистрации нам нужны имя и серийный номер, которые наверняка зависят от идентификатора системы (дальше HWID).

Вводим любые регистрационные данные, например:
Name: ds@mail.ru
Serial: abcdefghigklmnop

Смотрим в IDR код, который выполняется по щелчку на кнопку «Зарегистрировать», но видим, что кроме создание пары записей в реестре ничего больше не происходит:
image

image
Программа предлагает нам перезапустить ее, а это значит, что проверка выполняется где-то при старте программы. Предположим, проверка выполняется при создании главной формы (событие OnCreate). Ищем место вызова в IDR и ставим breakpoint в начало функции в Olly:
image

Стартуем программу из отладчика и останавливаемся здесь:
Copy Source | Copy HTML<br/>0046EC94 > . 55 PUSH EBP ; Unit1.TForm1.FormCreate <br/>

Доходим до первого интересного колла:
image
По F7 входим и трассируем дальше, пока не дойдем сюда:
image
Забегу немножко вперед и скажу, что именно здесь скрывается генерация HWID и вычиления первой части серийника.
Опять идем по F7 и определяем, что по адресу 46537F формируется HWID, о чем легко догадаться, пройдя функцию под отладчиком:
Copy Source | Copy HTML<br/>0046537F |. E8 3CFCFFFF CALL <QuickPas._Unit32.sub_00464FC0> ; << Get Hwid <br/>

image
А вот дальше внимательно исследуем функцию по адресу 00464FC0 и находим там код:
Copy Source | Copy HTML<br/>00465072 |> /8B45 FC /MOV EAX,DWORD PTR SS:[EBP-4]<br/>00465075 |. |0FB64418 FF |MOVZX EAX,BYTE PTR DS:[EAX+EBX-1]<br/>0046507A |. |8BD3 |MOV EDX,EBX<br/>0046507C |. |03D2 |ADD EDX,EDX<br/>0046507E |. |03C2 |ADD EAX,EDX<br/>00465080 |. |8D55 DC |LEA EDX,DWORD PTR SS:[EBP-24]<br/>00465083 |. |E8 3441FAFF |CALL <QuickPas.SysUtils.IntToStr><br/>00465088 |. |8B55 DC |MOV EDX,DWORD PTR SS:[EBP-24]<br/>0046508B |. |8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C]<br/>0046508E |. |E8 6D01FAFF |CALL <QuickPas.system.@LStrCat><br/>00465093 |. |43 |INC EBX<br/>00465094 |. |83FB 04 |CMP EBX,4<br/>00465097 |.^\75 D9 \JNZ SHORT QuickPas.00465072<br/>00465099 |. 8D45 F8 LEA EAX,DWORD PTR SS:[EBP-8]<br/>0046509C |. E8 93FEF9FF CALL <QuickPas.system.@LStrClr><br/>004650A1 |. BB 01000000 MOV EBX,1<br/>004650A6 |> 8D45 D8 /LEA EAX,DWORD PTR SS:[EBP-28]<br/>004650A9 |. 8B55 F4 |MOV EDX,DWORD PTR SS:[EBP-C]<br/>004650AC |. 0FB6541A FF |MOVZX EDX,BYTE PTR DS:[EDX+EBX-1]<br/>004650B1 |. E8 6600FAFF |CALL <QuickPas.system.@LStrFromChar><br/>004650B6 |. 8B55 D8 |MOV EDX,DWORD PTR SS:[EBP-28]<br/>004650B9 |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8]<br/>004650BC |. E8 3F01FAFF |CALL <QuickPas.system.@LStrCat><br/>004650C1 |. 43 |INC EBX<br/>004650C2 |. 83FB 06 |CMP EBX,6<br/>004650C5 |.^ 75 DF \JNZ SHORT QuickPas.004650A6 <br/>

Здесь берутся коды первых 3-х символов, + значение счетчика цикла, умноженного на 2, склеиваются в строку, а затем берутся только первые 5 (параметр SystemBiosDate, от которого считается хеш можно найти в реестре, а конкретнее здесь: HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System)
image
На паскале это можно записать так:
Copy Source | Copy HTML<br/>md5_sbd:=md5(от параметра SystemBiosDate)<br/>    for i:=1 to 3 do<br/>      begin<br/>        result:= result + IntToStr(ord(md5_sbd[i]) + i*2;);<br/>      end; <br/>

После выхода из функции получения HWID шагаем вперед:
Copy Source | Copy HTML<br/>00465389 |> /8D45 F8 /LEA EAX,DWORD PTR SS:[EBP-8]<br/>0046538C |. |E8 BF00FAFF |CALL <QuickPas.system.@UniqueStringA><br/>00465391 |. |BA 06000000 |MOV EDX,6<br/>00465396 |. |2BD3 |SUB EDX,EBX<br/>00465398 |. |8B4D FC |MOV ECX,DWORD PTR SS:[EBP-4]<br/>0046539B |. |0FB64C19 FF |MOVZX ECX,BYTE PTR DS:[ECX+EBX-1]<br/>004653A0 |. |884C10 FF |MOV BYTE PTR DS:[EAX+EDX-1],CL<br/>004653A4 |. |43 |INC EBX<br/>004653A5 |. |83FB 06 |CMP EBX,6<br/>004653A8 |.^\75 DF \JNZ SHORT QuickPas.00465389<br/>004653AA |. BB 01000000 MOV EBX,1<br/>004653AF |> 8D45 F0 /LEA EAX,DWORD PTR SS:[EBP-10]<br/>004653B2 |. 8B55 F8 |MOV EDX,DWORD PTR SS:[EBP-8]<br/>004653B5 |. 0FB6541A FF |MOVZX EDX,BYTE PTR DS:[EDX+EBX-1]<br/>004653BA |. E8 5DFDF9FF |CALL <QuickPas.system.@LStrFromChar><br/>004653BF |. 8B45 F0 |MOV EAX,DWORD PTR SS:[EBP-10]<br/>004653C2 |. E8 313FFAFF |CALL <QuickPas.SysUtils.StrToInt><br/>004653C7 |. 8BF0 |MOV ESI,EAX<br/>004653C9 |. 33F3 |XOR ESI,EBX<br/>004653CB |. 8D55 F4 |LEA EDX,DWORD PTR SS:[EBP-C]<br/>004653CE |. 8BC6 |MOV EAX,ESI<br/>004653D0 |. E8 E73DFAFF |CALL <QuickPas.SysUtils.IntToStr><br/>004653D5 |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8]<br/>004653D8 |. E8 7300FAFF |CALL <QuickPas.system.@UniqueStringA><br/>004653DD |. 8B55 F4 |MOV EDX,DWORD PTR SS:[EBP-C]<br/>004653E0 |. 0FB612 |MOVZX EDX,BYTE PTR DS:[EDX]<br/>004653E3 |. 885418 FF |MOV BYTE PTR DS:[EAX+EBX-1],DL<br/>004653E7 |. 43 |INC EBX<br/>004653E8 |. 83FB 06 |CMP EBX,6<br/>004653EB |.^ 75 C2 \JNZ SHORT QuickPas.004653AF <br/>

Здесь идут некоторые преобразования и т.д. Опять представляю переведенный мною данный участок кода только уже на паскале:
Copy Source | Copy HTML<br/>part_of_serial:=ReverseString(hwid);<br/>tmp:='';<br/>for i:= 1 to 5 do<br/>  begin<br/>  tmp:=tmp + copy(IntToStr(StrToInt(part_of_serial[i]) xor i),1,1);<br/>  end;<br/>part_of_serial:=copy(tmp,1,5); <br/>

В моем случае результат получился 41413
Идем дальше, видим…
image
Зная, что в программе используется хеш-функция md5, проверяем наше предположение:
md5(41413) = FFA54840A3C240E0725C16C7FA48281C
image
Это наглядно видно в окне стека:
image
Выходим из функции, продолжаем трейсить и видим, что далее берется md5-хеш от первых 5-ти символов введенного серийника и сравниваются с ранее полученным (примитивно, но все же хоть как-то).
md5(abcde) = AB56B4D92B40713ACC5AF89985D4B786
image
Теперь мы знаем, что первые символы правильного серийника (в моем случае) должны быть 41413.
Пробуем регистрировать программу с новым серийником с учетом вышесказанного:
41413abcdefghijklmnop, а мейл то же, что и раньше (ds@mail.ru).
Сейчас мы уже проходим проверку по адресу:
Copy Source | Copy HTML<br/>00465563 |. E8 DCFDF9FF CALL <QuickPas.system.@LStrCmp> <br/>

Программа запускается без предубреждения и в окне About пишет, что все хорошо, но вот при нажатии на функциональные кнопки в программе мы видим Unregistered version.

Ок, продолжаем с того места, где мы прошли проверку. Ставим breakpoint по адресу 00465563 и трассируем дальше.
Видим два очень простых куска кода:
Copy Source | Copy HTML<br/>00465595 |. BB 01000000 MOV EBX,1<br/>0046559A |> /8B45 EC /MOV EAX,DWORD PTR SS:[EBP-14]<br/>0046559D |. |0FB64418 FF |MOVZX EAX,BYTE PTR DS:[EAX+EBX-1]<br/>004655A2 |. |0145 E4 |ADD DWORD PTR SS:[EBP-1C],EAX<br/>004655A5 |. |43 |INC EBX<br/>004655A6 |. |83FB 06 |CMP EBX,6<br/>004655A9 |.^\75 EF \JNZ SHORT QuickPas.0046559A <br/>

Copy Source | Copy HTML<br/>004655BC |. BB 01000000 MOV EBX,1<br/>004655C1 |> 8B55 FC /MOV EDX,DWORD PTR SS:[EBP-4]<br/>004655C4 |. 0FB6541A FF |MOVZX EDX,BYTE PTR DS:[EDX+EBX-1]<br/>004655C9 |. 0155 E8 |ADD DWORD PTR SS:[EBP-18],EDX<br/>004655CC |. 43 |INC EBX<br/>004655CD |. 48 |DEC EAX<br/>004655CE |.^ 75 F1 \JNZ SHORT QuickPas.004655C1 <br/>

В них идут некоторые вычисления (сначала для символов с 6-го по 10 от введенного серийника), а потом от введенного e-mail.

В результате всего этого идет сравнение числа 1353 и fghijklmnop, логично, что это может быть третья часть серийного номера.
Пробуем новый серийный номер: 41413abcde1353
Вроде все отлично, но есть одно «НО»!
При клике по функциональным клавишам для восстановления мы получаем непонятные символы, типа:
image
Получается, что где-то что-то не так идет перед выводом результатов. Посмотрим в IDR событие по нажатию, например, на восстановления пароля The Bat и установим breakpoint в начало процедуры.
image
Затем заходим в функцию по адресу 465250 и внимательно смотрим в окно стека, а параллельно жмем F8
Сложно не заметить, что склеиваются первые 5 символов серийника и HWID, а затем от полученной строки считается md5-хеш.

md5(4141369735) = 3354B017EB74FB4DC20A1B48491D1431

А потом от полученного хеша считается некая сумма:

Copy Source | Copy HTML<br/>for i:=4 to 7 do<br/>  begin<br/>    tmp := tmp + IntToStr(ord(part2[i]));<br/>  end; <br/>


и копируются 5 первых символов:
Copy Source | Copy HTML<br/>some := copy(tmp,1,5); <br/>

В результате вычислений получаем 52664 (мы еще вернемся к этому значению).

Дальше интересный вызов:
CALL 00467A44
Для нас здесь нет ничего интересного (это основная функция программы, которая пытается восстановить пароль).
Идем дальше и заглядываем сюда:
00470919 |. E8 FA4DFFFF CALL <QuickPas._Unit32.sub_00465718>

Очень интересный блок:
Copy Source | Copy HTML<br/>0046577F |. BB 01000000 MOV EBX,1<br/>00465784 |> 8D45 FC /LEA EAX,DWORD PTR SS:[EBP-4]<br/>00465787 |. E8 C4FCF9FF |CALL <QuickPas.system.@UniqueStringA><br/>0046578C |. 8D4418 FF |LEA EAX,DWORD PTR DS:[EAX+EBX-1]<br/>00465790 |. 50 |PUSH EAX<br/>00465791 |. 8BC3 |MOV EAX,EBX<br/>00465793 |. 99 |CDQ<br/>00465794 |. F7FF |IDIV EDI<br/>00465796 |. 8B45 F8 |MOV EAX,DWORD PTR SS:[EBP-8]<br/>00465799 |. 0FB60410 |MOVZX EAX,BYTE PTR DS:[EAX+EDX]<br/>0046579D |. 0FBE55 F3 |MOVSX EDX,BYTE PTR SS:[EBP-D]<br/>004657A1 |. F7EA |IMUL EDX<br/>004657A3 |. 8B55 FC |MOV EDX,DWORD PTR SS:[EBP-4]<br/>004657A6 |. 0FB6541A FF |MOVZX EDX,BYTE PTR DS:[EDX+EBX-1]<br/>004657AB |. 03C2 |ADD EAX,EDX<br/>004657AD |. 5A |POP EDX<br/>004657AE |. 8802 |MOV BYTE PTR DS:[EDX],AL<br/>004657B0 |. 43 |INC EBX<br/>004657B1 |. 4E |DEC ESI<br/>004657B2 |.^ 75 D0 \JNZ SHORT QuickPas.00465784 <br/>

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

Идем дальше и видим:

Copy Source | Copy HTML<br/>0047093B |. E8 144DFFFF CALL <QuickPas._Unit32.sub_00465654> <br/>


Параметр, которые передается данной функции нам очень знаком — это символы серийника с 6-го по 10-ый.
Интуиция меня не подвела, видим такой кусок кода:

Copy Source | Copy HTML<br/>004656AC |. BB 01000000 MOV EBX,1<br/>004656B1 |> 8D45 FC /LEA EAX,DWORD PTR SS:[EBP-4]<br/>004656B4 |. E8 97FDF9FF |CALL <QuickPas.system.@UniqueStringA><br/>004656B9 |. 8D4418 FF |LEA EAX,DWORD PTR DS:[EAX+EBX-1]<br/>004656BD |. 50 |PUSH EAX<br/>004656BE |. 8BC3 |MOV EAX,EBX<br/>004656C0 |. 99 |CDQ<br/>004656C1 |. F7FF |IDIV EDI<br/>004656C3 |. 8B45 F8 |MOV EAX,DWORD PTR SS:[EBP-8]<br/>004656C6 |. 0FB60410 |MOVZX EAX,BYTE PTR DS:[EAX+EDX]<br/>004656CA |. 0FBE55 F3 |MOVSX EDX,BYTE PTR SS:[EBP-D]<br/>004656CE |. F7EA |IMUL EDX<br/>004656D0 |. 8B55 FC |MOV EDX,DWORD PTR SS:[EBP-4]<br/>004656D3 |. 0FB6541A FF |MOVZX EDX,BYTE PTR DS:[EDX+EBX-1]<br/>004656D8 |. 03C2 |ADD EAX,EDX<br/>004656DA |. 5A |POP EDX<br/>004656DB |. 8802 |MOV BYTE PTR DS:[EDX],AL<br/>004656DD |. 43 |INC EBX<br/>004656DE |. 4E |DEC ESI<br/>004656DF |.^ 75 D0 \JNZ SHORT QuickPas.004656B1 <br/>

Тот же код, но в данном случае он выполняет «дешифровку» сообщения перед самым его выводом.
Понятное дело, что для того, чтобы получить нормальные данные на выходе, нужно дешифровать с тем же ключом, что и шифровали.

true_key = 52664
our_key = abcde

message = decrypt(crypt(message,true_key),our_key)
Несложно догадаться, что для того, чтобы message=message, true_key должно равнятся our_key.

В общем все данные для кейгена у нас есть, осталось свести все в одну общую процедуру.
Для облегчения разобъем некоторые расчеты в отдельные функции:

1. Функция для получения значения из реестра:
Copy Source | Copy HTML<br/>function RegQueryStr(RootKey: HKEY; Key, Name: string;<br/>  Success: PBoolean = nil): string;<br/>var<br/>  Handle: HKEY;<br/>  Res: LongInt;<br/>  DataType, DataSize: DWORD;<br/>begin<br/>  if Assigned(Success) then<br/>    Success^ := False;<br/>  Res := RegOpenKeyEx(RootKey, PChar(Key),  0, KEY_QUERY_VALUE, Handle);<br/>  if Res <> ERROR_SUCCESS then<br/>    Exit;<br/>  Res := RegQueryValueEx(Handle, PChar(Name), nil, @DataType, nil, @DataSize);<br/>  if (Res <> ERROR_SUCCESS) or (DataType <> REG_SZ) then<br/>  begin<br/>    RegCloseKey(Handle);<br/>    Exit;<br/>  end;<br/>  SetString(Result, nil, DataSize - 1);<br/>  Res := RegQueryValueEx(Handle, PChar(Name), nil, @DataType,<br/>    PByte(@Result[1]), @DataSize);<br/>  if Assigned(Success) then<br/>    Success^ := Res = ERROR_SUCCESS;<br/>  RegCloseKey(Handle);<br/>end; <br/>


2. Функция вычисления HWID:
Copy Source | Copy HTML<br/>function GetHWID: string;<br/>var<br/>SystemBiosDate,md5_sbd:String;<br/>i:integer;<br/>digest:pointer;<br/>dwSize:Cardinal;<br/>begin<br/>  SystemBiosDate := RegQueryStr(HKEY_LOCAL_MACHINE, 'HARDWARE\DESCRIPTION\System', 'SystemBiosDate');<br/>    md5_sbd:=SystemBiosDate;<br/>      dwSize:=length(md5_sbd);<br/>  digest:= GetHash(@md5_sbd[1],dwSize,ALG_MD5);<br/>  if digest <> nil then<br/>    try<br/>    md5_sbd:=BinToHexStr(digest,dwSize);<br/>    finally<br/>    FreeMem(digest);<br/>    end;<br/>    md5_sbd:=Uppercase(md5_sbd);<br/>    result:='';<br/>    for i:=1 to 3 do<br/>      begin<br/>        result:= result + IntToStr(ord(md5_sbd[i]) + i*2);<br/>      end;<br/>result:=copy(Result,1,5); <br/>End; <br/>


3. Функция получения реверсивной (обратной строки):
Copy Source | Copy HTML<br/>function ReverseString(const s: string): string;<br/>var<br/>  i, len: Integer;<br/>begin<br/>  len := Length(s);<br/>  SetLength(Result, len);<br/>  for i := len downto 1 do<br/>  begin<br/>    Result[len - i + 1] := s[i];<br/>  end;<br/>end; <br/>


4. Функция для проверки, является ли строка числом (для корректности введенных данных пользователем):
Copy Source | Copy HTML<br/>function IsStrANumber(const S: string): Boolean;<br/>var<br/>  P: PChar;<br/>begin<br/>  P := PChar(S);<br/>  Result := False;<br/>  while P^ <> # 0 do<br/>  begin<br/>    if not (P^ in ['0'..'9']) then Exit;<br/>    Inc(P);<br/>  end;<br/>  Result := True;<br/>end; <br/>


Основная процедура генерации:
Copy Source | Copy HTML<br/>procedure generate;<br/>var Serial,NameBuf:String;<br/>len,len1:integer;<br/>Textname,HWID: PChar;<br/>hw:String;<br/>    i,sum_name,sum_part2:integer;<br/>    part1,tmp,part2,part3:string;<br/>    digest:pointer;<br/>dwSize:Cardinal;<br/>begin<br/>  len := GetWindowTextLengthA(TxtNameHwnd);<br/>  len1 := GetWindowTextLengthA(txtHWID);<br/>  if len > 1 then<br/>  begin<br/>  { Get text from name input }<br/>  GetMem(Textname, len);<br/>  GetWindowTextA(TxtNameHwnd,PAnsiChar(Textname),len + 1);<br/>  GetMem(HWID, len1);<br/>  GetWindowTextA(TxtHWID,PAnsiChar(HWID),len1 + 1);<br/>  HW:=String(HWID);<br/>if (IsStrANumber(hw)) and (length(hw)=5) then<br/>  begin<br/>  { Generate Serial }<br/>  NameBuf:=String(Textname);<br/>  hw:=string(HWID);<br/>hw:=copy(hw,1,5);<br/>part1:=ReverseString(hw);<br/>tmp:='';<br/>for i:= 1 to 5 do<br/>  begin<br/>  tmp:=tmp + copy(IntToStr(StrToInt(part1[i]) xor i),1,1);<br/>  end;<br/>part1:=copy(tmp,1,5);<br/>//---------------------<br/>tmp:='';<br/>part2 :=part1+hw;<br/>  dwSize:=length(part2);<br/>  digest:= GetHash(@part2[1],dwSize,ALG_MD5);<br/>  if digest <> nil then<br/>    try<br/>    part2:=BinToHexStr(digest,dwSize);<br/>    finally<br/>    FreeMem(digest);<br/>    end;<br/>part2 :=UpperCase(part2);<br/>for i:=4 to 7 do<br/>  begin<br/>    tmp := tmp + IntToStr(ord(part2[i]));<br/>  end;<br/>part2 := copy(tmp,1,5);;<br/>//---------------------<br/>sum_name:= 0;<br/>sum_part2:= 0;<br/>for i:=1 to length(NameBuf)-1 do<br/>  begin<br/>  sum_name:=sum_name + ord(NameBuf[i]);<br/>  end;<br/>for i:=1 to length(part2) do<br/>  begin<br/>  sum_part2:=sum_part2 + ord(part2[i]);<br/>  end;<br/>part3:=IntToStr(sum_name+sum_part2);<br/>Serial:=part1 + part2 + part3;<br/>  end<br/>  else // if hwid not numeric or <> 5<br/>  Serial:='Invalid HWID';<br/>  { Display The Results }<br/>  SetWindowTextA(TxtSerialHwnd,PChar(Serial));<br/>  FreeMem (HWID, len1 + 1);<br/>  FreeMem (Textname, len+1);<br/>  end<br/>  Else<br/>  { Display Error }<br/>       SetWindowText(TxtSerialHwnd,'Not Enough Characters..');<br/>end; <br/>


Думаю, здесь все предельно ясно, так как ранее были разобраны все основные моменты.
Могут вызвать вопросы вот эти участки кода:
Copy Source | Copy HTML<br/>dwSize:=length(part2);<br/>digest:= GetHash(@part2[1],dwSize,ALG_MD5);<br/>if digest <> nil then<br/>  try<br/>  part2:=BinToHexStr(digest,dwSize);<br/>  finally<br/>  FreeMem(digest);<br/>  end; <br/>

Если вкратце, то это вычисление md5-хеша с использованием HashCryptoAPILib, которая в свою очередь использует стандартные средства Windows (CryptoApi). Можете скомпилировать кейген сами или взять уже готовый на tport.org в разделе «Download».

Генерируем серийник на любое имя и тестируем. Программа зарегистрирована, работает правильно, пытается выполнять свои функции и выводит сообщения в нормальном виде. На этом все.

Надеюсь, что вышеизложенный материал был интересен и кто-то подчерпнет что-нибудь полезное для себя.
Tags:
Hubs:
+39
Comments10

Articles