Исследование защиты программы Visual Watermark


Скриншот программы Visual Watermark

Программа Visual Watermark предназначена для нанесения текстовых и графических водяных знаков или подписей на цифровые фотографии. Программа очень проста в использовании, имеет простой и дружественный интерфейс, поддерживает все популярные графические форматы, имеет функцию пакетной обработки изображений. Все это можно сделать совершенно бесплатно в любых графических программах, а можно выложить денежку и сделать в программе Visual Watermark. А можно и не выкладывать ничего, и точно так же сделать в программе Visual Watermark. Последний вариант мне нравится больше.

Скачиваем дистрибутив, устанавливаем, запускаем. Исполняемый файл ничем не упакован, поэтому параллельно открываем его на анализ в дизассемблере. А пока посмотрим, как программа реагирует на неправильную регистрацию:


Сообщение о неправильной регистрации

Нужная строка легко находится в файле:


Строка сообщения найдена

Дальше в дизассемблере по перекрестным ссылкам находим код, где она используется и смотрим условия ее появления:
.text:00457360 sub_457360      proc near
.text:00457360 arg_0           = dword ptr  8
.text:00457360                 push    ebp
.text:00457361                 mov     ebp, esp
.text:00457363                 mov     eax, [ebp+arg_0]
.text:00457366                 inc     dword ptr [eax+30Ch]
.text:0045736C                 mov     edx, [ebp+arg_0]
.text:0045736F                 cmp     dword ptr [edx+30Ch], 3
.text:00457376                 jge     short loc_457394
.text:00457378                 push    10h             ; uType
.text:0045737A                 push    offset aRegistration_1
; "Registration"
.text:0045737F                 push    offset aTheKeyYouEnter
; "The key you entered is not valid. Pleas"...
.text:00457384                 mov     eax, [ebp+arg_0]
.text:00457387                 call    @Controls@TWinControl@GetHandle$qqrv
.text:0045738C                 push    eax             ; hWnd
.text:0045738D                 call    MessageBoxA
.text:00457392                 pop     ebp
.text:00457393                 retn

Вывод сообщения о неправильной регистрации оформлен в виде отдельной процедуры, которая, в свою очередь, вызывается отсюда:
; Вызвать процедуру проверки
.text:004572B5                 call    sub_42920C
.text:004572BA                 pop     ecx
; ПоXORить результат функции с константой 35h
.text:004572BB                 xor     eax, 35h
; Сохранить результат в ячейку памяти
.text:004572BE                 mov     [ebp+var_44], eax
.text:004572C1                 mov     [ebp+var_28], 14h
; Сравнить результат с числом 24h
.text:004572C7                 cmp     [ebp+var_44], 24h
; Если не равно, то показать сообщение о неправильной регистрации
.text:004572CB                 jz      short loc_4572D8
.text:004572CD                 push    [ebp+var_3C]
; Сообщение о неправильной регистрации
.text:004572D0                 call    sub_457360

Вызывается процедура проверки по адресу 0042920C, если она вернула значение, которое после XORирования с константой 35h дает значение 24h, то регистрация правильная. Несложной математикой получаем нужное значение - 11h. Сама же функция проверки достаточно громоздкая, но тут нам на помощь приходит замечательный плагин для дизассемблера IDA - Hex-Rays. Он позволяет перевести ассемблерный листинг в C-подобный псевдокод. Скомпилировать его, конечно, не получится, но зато псевдокод в разы облегчает анализ файла. Вот что получается после преобразования функции проверки в псевдокод:
int __cdecl sub_42920C(char a1)
{
  __InitExceptBlockLDTC();
  System__AnsiString__AnsiString(&a1, &a1);
  v12 = 2;
  v11 = 8;
  v9 = sub_42919C(a1);
  if ( sub_429350(&unk_6D71F8, &unk_6E0E38, &v9) > (unsigned int)&unk_6E0E34
    || (v8 = sub_42919C(a1),
        v2 = __rwstd____RW_array(&unk_732B08),
        v3 = unknown_libname_34(&unk_732B08),
        v4 = sub_429350(v3, v2, &v8),
        v4 != __rwstd____RW_array(&unk_732B08))
    || System__AnsiString__Length(&a1, v1, v7) != 19
    || *(_BYTE *)System__AnsiString__operator__(&a1, 5) != 45
    || *(_BYTE *)System__AnsiString__operator__(&a1, 10) != 45
    || *(_BYTE *)System__AnsiString__operator__(&a1, 15) != 45 )
    v10 = 6681;
  else
    v10 = 17;
  v6 = v10;
  --v12;
  sub_6C8040(&a1, 2);
  return v6;
}

Все как на ладони. Тут и проверка на длину серийника - 19 символов, и проверка на наличие разделителей (символов с кодами 45 в десятичной системе, это символы "-") в позициях 5, 10 и 15. А также еще одна дополнительная проверка, к которой мы вернемся чуть позже. А пока мы выяснили формат и длину серийного номера, это должно быть что-то типа "ABCD-EFGH-JKLM-NOPQ". Теперь нас интересуют два вызова - это функция sub_42919C и sub_429350. Первая представляет собой последовательность вызовов:
.text:0042919C sub_42919C      proc near
.text:0042919C                 push    ebp
.text:0042919D                 mov     ebp, esp
.text:0042919F                 add     esp, 0FFFFFFD4h
.text:004291A2                 mov     eax, offset stru_6E0F50
.text:004291A7                 call    @__InitExceptBlockLDTC
.text:004291AC                 mov     [ebp+var_8], 1
.text:004291B3                 lea     edx, [ebp+arg_0]
.text:004291B6                 lea     eax, [ebp+arg_0]
.text:004291B9                 call    @System@AnsiString@AnsiString
.text:004291BE                 inc     [ebp+var_8]
.text:004291C1                 mov     [ebp+var_14], 8
.text:004291C7                 lea     eax, [ebp+arg_0]
.text:004291CA                 call    @System@AnsiString@c_str$xqqrv
.text:004291CF                 push    eax
.text:004291D0                 lea     edx, [ebp+var_2C]
.text:004291D3                 push    edx
.text:004291D4                 call    sub_4291FC
.text:004291D9                 add     esp, 8
.text:004291DC                 push    eax
.text:004291DD                 dec     [ebp+var_8]
.text:004291E0                 lea     eax, [ebp+arg_0]
.text:004291E3                 mov     edx, 2
.text:004291E8                 call    sub_6C8040
.text:004291ED                 pop     eax
.text:004291EE                 mov     edx, [ebp+var_24]
.text:004291F1                 mov     large fs:0, edx
.text:004291F8                 mov     esp, ebp
.text:004291FA                 pop     ebp
.text:004291FB                 retn
.text:004291FB sub_42919C      endp
 
;-------------------------------------------------------
 
.text:004291FC sub_4291FC      proc near
.text:004291FC                 push    ebp
.text:004291FD                 mov     ebp, esp
.text:004291FF                 push    [ebp+arg_4]
.text:00429202                 call    sub_429FC0
.text:00429207                 pop     ecx
.text:00429208                 pop     ebp
.text:00429209                 retn
.text:00429209 sub_4291FC      endp
 
;-------------------------------------------------------
 
.text:00429FC0                 push    ebp
.text:00429FC1                 mov     ebp, esp
.text:00429FC3                 push    ecx
.text:00429FC4                 xor     eax, eax
.text:00429FC6                 mov     [ebp+var_4], eax
.text:00429FC9                 jmp     short loc_429FDF
.text:00429FCB ; ---------------------------------
.text:00429FCB loc_429FCB:
.text:00429FCB                 mov     edx, [ebp+var_4]
.text:00429FCE                 lea     edx, [edx+edx*4]
.text:00429FD1                 mov     ecx, [ebp+arg_0]
.text:00429FD4                 movsx   eax, byte ptr [ecx]
.text:00429FD7                 add     edx, eax
.text:00429FD9                 mov     [ebp+var_4], edx
.text:00429FDC                 inc     [ebp+arg_0]
.text:00429FDF loc_429FDF:
.text:00429FDF                 mov     edx, [ebp+arg_0]
.text:00429FE2                 cmp     byte ptr [edx], 0
.text:00429FE5                 jnz     short loc_429FCB
.text:00429FE7                 mov     eax, [ebp+var_4]
.text:00429FEA                 pop     ecx
.text:00429FEB                 pop     ebp
.text:00429FEC                 retn

При помощи того же плагина Hex-Rays этот код сворачивается в абсолютно понятный псевдокод:
int __cdecl sub_429FC0(int a1)
{
  int v2; // [sp+0h] [bp-4h]@1
 
  v2 = 0;
  while ( *(_BYTE *)a1 )
    v2 = *(_BYTE *)a1++ + 5 * v2;
  return v2;
}

Под отладчиком эта процедура становится еще понятнее, в ней считается некая контрольная сумма от всего серийного номера, включая разделители. Алгоритм заключается в последовательном сложении и умножении на 5 всех символов строки серийного номера. На выходе получаем контрольное значение. Вторая функция - это проверка контрольного значения. Я приведу только ее часть, остальное лучше смотреть под отладчиком.
.text:00429350 sub_429350      proc near
.text:00429350                 push    ebp
.text:00429351                 mov     ebp, esp
.text:00429353                 add     esp, 0FFFFFFF8h
.text:00429356                 lea     eax, [ebp+var_8]
.text:00429359                 push    eax
.text:0042935A                 push    [ebp+arg_8]
.text:0042935D                 push    [ebp+arg_4]
.text:00429360                 push    [ebp+arg_0]
.text:00429363                 call    sub_429370
.text:00429368                 add     esp, 10h
.text:0042936B                 pop     ecx
.text:0042936C                 pop     ecx
.text:0042936D                 pop     ebp
.text:0042936E                 retn
.text:0042936E sub_429350      endp
 
;-------------------------------------------------------
 
.text:00429370 sub_429370      proc near
.text:00429370                 push    ebp
.text:00429371                 mov     ebp, esp
.text:00429373                 push    ecx
.text:00429374                 mov     eax, [ebp+arg_4]
.text:00429377                 sub     eax, [ebp+arg_0]
.text:0042937A                 test    eax, eax
.text:0042937C                 jns     short loc_429381
.text:0042937E                 add     eax, 3
.text:00429381 loc_429381:
.text:00429381                 sar     eax, 2
.text:00429384                 sar     eax, 2
.text:00429387                 mov     [ebp+var_4], eax
.text:0042938A                 cmp     [ebp+var_4], 0
.text:0042938E                 jle     short loc_4293F1
.text:00429390 loc_429390:
.text:00429390                 mov     edx, [ebp+arg_0]
.text:00429393                 mov     ecx, [edx]
.text:00429395                 mov     eax, [ebp+arg_8]
.text:00429398                 cmp     ecx, [eax]
.text:0042939A                 jnz     short loc_4293A2
.text:0042939C                 mov     eax, [ebp+arg_0]
.text:0042939F                 pop     ecx
.text:004293A0                 pop     ebp
.text:004293A1                 retn
.text:004293A2 ; ------------------------------------------
.text:004293A2 loc_4293A2:
.text:004293A2                 add     [ebp+arg_0], 4
.text:004293A6                 mov     edx, [ebp+arg_0]
.text:004293A9                 mov     ecx, [edx]
.text:004293AB                 mov     eax, [ebp+arg_8]
.text:004293AE                 cmp     ecx, [eax]
.text:004293B0                 jnz     short loc_4293B8
.text:004293B2                 mov     eax, [ebp+arg_0]
.text:004293B5                 pop     ecx
.text:004293B6                 pop     ebp
.text:004293B7                 retn
.text:004293B8 ; ----------------------------------------------
.text:004293B8 loc_4293B8:
.text:004293B8                 add     [ebp+arg_0], 4
.text:004293BC                 mov     edx, [ebp+arg_0]
.text:004293BF                 mov     ecx, [edx]
.text:004293C1                 mov     eax, [ebp+arg_8]
.text:004293C4                 cmp     ecx, [eax]
.text:004293C6                 jnz     short loc_4293CE
.text:004293C8                 mov     eax, [ebp+arg_0]
.text:004293CB                 pop     ecx
.text:004293CC                 pop     ebp
.text:004293CD                 retn
 
...

В листинге код не очень понятен, а вот отладчик вносит ясность. В программе по адресу 006D71F8 содержится массив заранее определенных проверочных значений для серийных номеров в формате DWORD размером 10000 элементов:
.data:006D71F8 dword_6D71F8    dd 0C6DDDh
.data:006D71FC                 dd 1267C1h
.data:006D7200                 dd 14EC36h
.data:006D7204                 dd 15BFAFh
.data:006D7208                 dd 1F2B5Bh
.data:006D720C                 dd 259E5Ch
.data:006D7210                 dd 3F92A6h
.data:006D7214                 dd 47A33Eh
...

Если рассчитанное проверочное значение совпадает с одним из значений их массива, то программа считается зарегистрированной. То есть какого-либо особого алгоритма подсчета серийного номера нет, все возможные значения заранее определены. Итак, мы знаем формат серийного номера, алгоритм подсчета контрольного значения и массив из 10000 проверочных значений. Кейген пишется на базе простейшего брутфорса.


Программа успешно зарегистрирована

Защиты такого типа, где серийный номер сравнивается с заранее подготовленным списком, встречаются не очень часто, но все-таки авторы упорно продолжают их использовать. И хорошо, если для хеширования используется что-то типа MD5 или более серьезные алгоритмы, а что если при слабом алгоритме хеширования ключ из списка будет подобран брутфорсом или реверсом алгоритма из исходного значения? Автор, по идее, должен забанить такие ключи сразу после релиза. Что же в этом случае делать легальным пользователям? Вопрос остается без ответа.
Добавлено: 12 Сентября 2013 11:20:32 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...